Skip to content
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10263,6 +10263,7 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
"egui_extras",
"egui_kittest",
"egui_plot",
"ehttp",
Expand Down
10 changes: 10 additions & 0 deletions crates/utils/re_auth/src/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ pub fn load_credentials() -> Result<Option<Credentials>, CredentialsLoadError> {
}
}

#[derive(Debug, thiserror::Error)]
#[error("failed to load credentials: {0}")]
pub struct CredentialsClearError(#[from] storage::ClearError);

pub fn clear_credentials() -> Result<(), CredentialsClearError> {
storage::clear()?;

Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum CredentialsRefreshError {
#[error("failed to refresh credentials: {0}")]
Expand Down
47 changes: 43 additions & 4 deletions crates/utils/re_auth/src/oauth/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,29 @@ pub enum StoreError {
NoLocalStorage,
}

#[derive(Debug, thiserror::Error)]
pub enum ClearError {
#[error("failed to clear credentials: {0}")]
Io(#[from] std::io::Error),

#[cfg(not(target_arch = "wasm32"))]
#[error("could not find a valid config location, please ensure $HOME is set")]
UnknownConfigLocation,

#[cfg(target_arch = "wasm32")]
#[error("failed to get window.localStorage")]
NoLocalStorage,
}

#[cfg(not(target_arch = "wasm32"))]
pub use file::{load, store};
pub use file::{clear, load, store};

#[cfg(target_arch = "wasm32")]
pub use web::{load, store};
pub use web::{clear, load, store};

#[cfg(not(target_arch = "wasm32"))]
mod file {
use super::{Credentials, LoadError, StoreError};
use super::{ClearError, Credentials, LoadError, StoreError};
use std::path::PathBuf;

fn credentials_path() -> Option<PathBuf> {
Expand All @@ -71,13 +85,24 @@ mod file {
std::fs::write(path, data)?;
Ok(())
}

pub fn clear() -> Result<(), ClearError> {
let path = credentials_path().ok_or(ClearError::UnknownConfigLocation)?;

match std::fs::remove_file(path) {
Ok(()) => Ok(()),
// If the file didn't exist this isn't a failure.
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(ClearError::Io(err)),
}
}
}

#[cfg(target_arch = "wasm32")]
mod web {
use wasm_bindgen::JsCast as _;

use super::{Credentials, LoadError, StoreError};
use super::{ClearError, Credentials, LoadError, StoreError};

const STORAGE_KEY: &str = "rerun_auth";

Expand All @@ -95,6 +120,12 @@ mod web {
}
}

impl From<NoLocalStorage> for ClearError {
fn from(_: NoLocalStorage) -> Self {
Self::NoLocalStorage
}
}

#[expect(clippy::needless_pass_by_value)]
pub fn string_from_js_value(s: wasm_bindgen::JsValue) -> String {
// it's already a string
Expand Down Expand Up @@ -140,4 +171,12 @@ mod web {
.map_err(|err| std::io::Error::other(string_from_js_value(err)))?;
Ok(())
}

pub fn clear() -> Result<(), ClearError> {
let local_storage = get_local_storage()?;
local_storage
.remove_item(STORAGE_KEY)
.map_err(|err| std::io::Error::other(string_from_js_value(err)))?;
Ok(())
}
}
14 changes: 10 additions & 4 deletions crates/viewer/re_redap_browser/src/server_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub enum ServerModalMode {
}

/// Authentication state for the server modal.
struct Authentication {
email: Option<String>,
pub(crate) struct Authentication {
pub(crate) email: Option<String>,
token: String,
show_token_input: bool,
login_flow: Option<LoginFlow>,
Expand Down Expand Up @@ -93,7 +93,7 @@ pub struct ServerModal {
mode: ServerModalMode,
scheme: Scheme,
host: String,
auth: Authentication,
pub(crate) auth: Authentication,
port: u16,
}

Expand All @@ -104,7 +104,8 @@ impl Default for ServerModal {
mode: ServerModalMode::Add,
scheme: Scheme::Rerun,
host: String::new(),
auth: Authentication::new(None, false),
// use_stored_credentials is true so we can read the email in the welcome screen
auth: Authentication::new(None, true),
port: 443,
}
}
Expand Down Expand Up @@ -150,6 +151,11 @@ impl ServerModal {
self.modal.open();
}

pub fn logout(&mut self) {
self.auth.email = None;
self.auth.reset_login_flow();
}

pub fn ui(&mut self, global_ctx: &GlobalContext<'_>, ctx: &Context<'_>, ui: &egui::Ui) {
let was_open = self.modal.is_open();

Expand Down
21 changes: 21 additions & 0 deletions crates/viewer/re_redap_browser/src/servers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,21 @@ impl RedapServers {
self.servers.values()
}

pub fn is_authenticated(&self, origin: &re_uri::Origin) -> bool {
self.servers
.get(origin)
.and_then(|server| server.connection_registry.credentials(origin))
.is_some()
}

pub fn logout(&mut self) {
self.server_modal_ui.logout();
}

pub fn auth_email(&self) -> Option<String> {
self.server_modal_ui.auth.email.clone()
}

/// Per-frame housekeeping.
///
/// - Process commands from the queue.
Expand Down Expand Up @@ -452,6 +467,12 @@ impl RedapServers {
self.command_sender.send(Command::OpenAddServerModal).ok();
}

pub fn open_edit_server_modal(&self, origin: re_uri::Origin) {
self.command_sender
.send(Command::OpenEditServerModal(origin))
.ok();
}

pub fn entry_ui(
&self,
viewer_ctx: &ViewerContext<'_>,
Expand Down
2 changes: 2 additions & 0 deletions crates/viewer/re_test_context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,10 +749,12 @@ impl TestContext {
| SystemCommand::ClearActiveBlueprint
| SystemCommand::ClearActiveBlueprintAndEnableHeuristics
| SystemCommand::AddRedapServer { .. }
| SystemCommand::EditRedapServerModal { .. }
| SystemCommand::UndoBlueprint { .. }
| SystemCommand::RedoBlueprint { .. }
| SystemCommand::CloseAllEntries
| SystemCommand::SetAuthCredentials { .. }
| SystemCommand::Logout
| SystemCommand::ShowNotification { .. } => handled = false,

#[cfg(debug_assertions)]
Expand Down
141 changes: 141 additions & 0 deletions crates/viewer/re_ui/src/button.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use crate::{DesignTokens, UiExt as _};
use egui::style::WidgetVisuals;
use egui::{Button, CornerRadius, IntoAtoms, Style};

pub enum Variant {
Primary,
Secondary,
Ghost,
}

pub enum Size {
Normal,
Small,
}

impl Size {
pub fn apply(&self, style: &mut Style) {
match self {
Self::Normal => {
style.spacing.button_padding = egui::vec2(12.0, 8.0);
all_visuals(style, |vis| {
vis.corner_radius = CornerRadius::same(6);
});
}
Self::Small => {
style.spacing.button_padding = egui::vec2(8.0, 4.0);
all_visuals(style, |vis| {
vis.corner_radius = CornerRadius::same(3);
});
}
}
}
}

fn all_visuals(style: &mut Style, f: impl Fn(&mut WidgetVisuals)) {
f(&mut style.visuals.widgets.active);
f(&mut style.visuals.widgets.hovered);
f(&mut style.visuals.widgets.inactive);
f(&mut style.visuals.widgets.noninteractive);
f(&mut style.visuals.widgets.open);
}

impl Variant {
pub fn apply(&self, style: &mut Style, tokens: &DesignTokens) {
match self {
Self::Primary => {
all_visuals(style, |vis| {
vis.bg_fill = tokens.bg_fill_inverse;
vis.weak_bg_fill = tokens.bg_fill_inverse;
vis.fg_stroke.color = tokens.text_inverse;
});
style.visuals.widgets.hovered.bg_fill = tokens.bg_fill_inverse_hover;
style.visuals.widgets.hovered.weak_bg_fill = tokens.bg_fill_inverse_hover;
}
Self::Secondary => {
all_visuals(style, |vis| {
vis.bg_fill = tokens.widget_active_bg_fill;
vis.weak_bg_fill = tokens.widget_active_bg_fill;
});
style.visuals.widgets.hovered.bg_fill = tokens.widget_noninteractive_bg_fill;
style.visuals.widgets.hovered.weak_bg_fill = tokens.widget_noninteractive_bg_fill;
}
Self::Ghost => {
// The default button
}
}
}
}

pub struct ReButton<'a> {
pub variant: Variant,
pub size: Size,
pub inner: Button<'a>,
}

impl<'a> ReButton<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms))
}

pub fn from_button(button: Button<'a>) -> Self {
ReButton {
inner: button,
size: Size::Normal,
variant: Variant::Ghost,
}
}

pub fn primary(mut self) -> Self {
self.variant = Variant::Primary;
self
}

pub fn secondary(mut self) -> Self {
self.variant = Variant::Secondary;
self
}

pub fn ghost(mut self) -> Self {
self.variant = Variant::Ghost;
self
}

pub fn small(mut self) -> Self {
self.size = Size::Small;
self
}

pub fn normal(mut self) -> Self {
self.size = Size::Normal;
self
}
}

pub trait ReButtonExt<'a> {
fn primary(self) -> ReButton<'a>;
fn secondary(self) -> ReButton<'a>;
}

impl<'a> ReButtonExt<'a> for Button<'a> {
fn primary(self) -> ReButton<'a> {
ReButton::from_button(self).primary()
}

fn secondary(self) -> ReButton<'a> {
ReButton::from_button(self).secondary()
}
}

impl egui::Widget for ReButton<'_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let previous_style = ui.style().clone();
let tokens = ui.tokens();
let style = ui.style_mut();
self.size.apply(style);
self.variant.apply(style, tokens);
let response = ui.add(self.inner);
ui.set_style(previous_style);
response
}
}
Loading
Loading