Files
mqttx/gpui-app/src/ui/aliyun_dialog.rs
T
JoeAllen bf6937b776 feat: 重写为 GPUI 原生桌面应用并接入 Gitea 软件包发布
- gpui-kit 0.6 原生 UI:连接管理、消息流/日志、发布预设、全局变量、
  设置、阿里云连接预设、原生 Logo
- MQTT 5.0/3.1.1 引擎(rumqttc,TCP/TLS/WS/WSS),独立 tokio runtime
  + 事件泵,真实 broker 端到端测试
- 连接表单:保存前可一键测试连接(宽松校验,自动生成临时 Client ID),
  高级属性(认证/MQTT 5 属性/遗嘱)按需折叠显示,WS 路径仅 WS/WSS 显示
- bug 修复:Hex 奇数长度显式报错、测试连接唯一 Client ID、Base64/Hex
  发布发送真实字节、v5 发布记录携带 Content-Type/用户属性、预设应用
  清除旧 Content-Type、模板时间基准统一;补 8 个单元测试
- Gitea 发布:Generic Package 上传脚本(bash/PowerShell)+ Actions
  release 工作流(tag 触发,双平台构建),README 使用说明
2026-09-28 11:39:47 +08:00

299 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 阿里云 IoT 设备接入对话框:保存预设,并可一键生成连接并连接。
use gpui_kit::component::button::{Button, ButtonVariants as _};
use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::select::{Select, SelectState};
use gpui_kit::component::{
h_flex, notification::Notification, v_flex, ActiveTheme as _, Sizable as _, StyledExt as _,
WindowExt as _,
};
use gpui_kit::prelude::FluentBuilder as _;
use crate::ui::IconName;
use gpui_kit::{
div, px, App, AppContext as _, Context, Entity, InteractiveElement as _, StatefulInteractiveElement as _,
IntoElement, ParentElement as _, Render, Styled as _, Window,
};
use crate::aliyun::{AliyunAuthMode, AliyunPreset};
use crate::model::ConnectionConfig;
use crate::ui::app::MqttXApp;
use crate::ui::widgets::{field, make_select, OptionDelegate};
const AUTH_MODES: [&str; 2] = ["Group 令牌 (Token)", "一机一密"];
struct AliyunDialog {
app: Entity<MqttXApp>,
presets: Vec<AliyunPreset>,
selected: Option<usize>,
name: Entity<InputState>,
group: Entity<InputState>,
instance_id: Entity<InputState>,
region: Entity<InputState>,
auth_mode: Entity<SelectState<OptionDelegate>>,
access_key_id: Entity<InputState>,
access_key_secret: Entity<InputState>,
group_id: Entity<InputState>,
device_id: Entity<InputState>,
device_secret: Entity<InputState>,
}
macro_rules! text_field {
($cx:expr, $window:expr, $value:expr, $ph:expr) => {
$cx.new(|cx| {
InputState::new($window, cx)
.default_value($value)
.placeholder($ph)
})
};
}
impl AliyunDialog {
fn new(app: Entity<MqttXApp>, presets: Vec<AliyunPreset>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let first = presets.first().cloned().unwrap_or_else(AliyunPreset::new);
Self {
app,
presets,
selected: None,
name: text_field!(cx, window, &first.name, "预设名称"),
group: text_field!(cx, window, &first.group, "分组(可选)"),
instance_id: text_field!(cx, window, &first.instance_id, "post-cn-xxxx"),
region: text_field!(cx, window, &first.region, "如 cn-shanghai(可留空)"),
auth_mode: make_select(
&AUTH_MODES,
match first.auth_mode {
AliyunAuthMode::Token => 0,
AliyunAuthMode::DeviceCredential => 1,
},
window,
cx,
),
access_key_id: text_field!(cx, window, &first.access_key_id, "AccessKey ID"),
access_key_secret: cx.new(|cx| {
InputState::new(window, cx)
.default_value(&first.access_key_secret)
.placeholder("AccessKey Secret")
.masked(true)
}),
group_id: text_field!(cx, window, &first.group_id, "GID-xxx"),
device_id: text_field!(cx, window, &first.device_id, "设备 ID"),
device_secret: cx.new(|cx| {
InputState::new(window, cx)
.default_value(&first.device_secret)
.placeholder("一机一密:DeviceSecret")
.masked(true)
}),
}
}
fn collect(&self, cx: &App) -> AliyunPreset {
let v = |e: &Entity<InputState>| e.read(cx).value().to_string();
let id = self
.selected
.and_then(|i| self.presets.get(i))
.map(|p| p.id.clone())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
AliyunPreset {
id,
name: v(&self.name),
group: v(&self.group),
instance_id: v(&self.instance_id),
region: v(&self.region),
auth_mode: match self.auth_mode.read(cx).selected_value() {
Some(1) => AliyunAuthMode::DeviceCredential,
_ => AliyunAuthMode::Token,
},
access_key_id: v(&self.access_key_id),
access_key_secret: v(&self.access_key_secret),
group_id: v(&self.group_id),
device_id: v(&self.device_id),
device_secret: v(&self.device_secret),
}
}
fn save_preset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let preset = self.collect(cx);
if preset.name.trim().is_empty() {
window.push_notification(Notification::warning("请填写预设名称"), cx);
return;
}
match self.selected {
Some(i) if i < self.presets.len() => self.presets[i] = preset,
_ => self.presets.push(preset),
}
let saved = self.presets.clone();
self.app.update(cx, |a, cx| {
a.save_aliyun(saved);
cx.notify();
});
window.push_notification(Notification::success("预设已保存"), cx);
}
fn connect(&self, window: &mut Window, cx: &mut Context<Self>) {
let preset = self.collect(cx);
match preset.to_connection() {
Ok(conn) => {
let mut conn: ConnectionConfig = conn;
conn.name = preset.name.clone();
self.app.update(cx, |a, cx| {
a.save_connection(conn.clone());
a.engine.connect(conn);
cx.notify();
});
window.close_dialog(cx);
}
Err(e) => {
window.push_notification(Notification::error(format!("无法生成连接: {e}")), cx);
}
}
}
}
impl Render for AliyunDialog {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_3()
.w_full()
// 左:预设列表
.child(
v_flex()
.w(px(160.))
.gap_1()
.child(div().text_xs().font_semibold().child("已保存预设"))
.children(
self.presets
.iter()
.enumerate()
.map(|(i, p)| {
let selected = self.selected == Some(i);
div()
.id(("aliyun-preset", i))
.px_2()
.py_1()
.rounded_md()
.text_sm()
.when(selected, |d| d.bg(cx.theme().secondary))
.cursor_pointer()
.child(p.name.clone())
.on_click(cx.listener(move |this, _, _, cx| {
// 选中仅做高亮;表单编辑的是“新建/另存”工作流
this.selected = Some(i);
cx.notify();
}))
}),
)
.child(
Button::new("aliyun-new")
.icon(IconName::Plus)
.label("新建")
.ghost()
.small()
.on_click(cx.listener(|this, _, _, cx| {
this.selected = None;
cx.notify();
})),
),
)
// 右:表单
.child(
v_flex()
.flex_1()
.min_w(px(0.))
.gap_2()
.child(field("预设名称", Input::new(&self.name)))
.child(
h_flex()
.gap_2()
.child(
div().flex_1().min_w(px(0.)).child(field(
"实例 ID",
Input::new(&self.instance_id),
)),
)
.child(
div().flex_1().min_w(px(0.)).child(field(
"地域",
Input::new(&self.region),
)),
),
)
.child(field("鉴权方式", Select::new(&self.auth_mode)))
.child(
h_flex()
.gap_2()
.child(
div().flex_1().min_w(px(0.)).child(field(
"AccessKey ID",
Input::new(&self.access_key_id),
)),
)
.child(
div().flex_1().min_w(px(0.)).child(field(
"AccessKey Secret",
Input::new(&self.access_key_secret).mask_toggle(),
)),
),
)
.child(
h_flex()
.gap_2()
.child(
div().flex_1().min_w(px(0.)).child(field(
"Group ID",
Input::new(&self.group_id),
)),
)
.child(
div().flex_1().min_w(px(0.)).child(field(
"设备 ID",
Input::new(&self.device_id),
)),
),
)
.child(field(
"DeviceSecret(一机一密)",
Input::new(&self.device_secret).mask_toggle(),
))
.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Token 模式使用 MQTT 3.1.1,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名"),
)
.child(
h_flex()
.gap_2()
.justify_end()
.child(
Button::new("aliyun-save")
.label("保存预设")
.outline()
.on_click(cx.listener(|this, _, window, cx| {
this.save_preset(window, cx)
})),
)
.child(
Button::new("aliyun-connect")
.label("生成并连接")
.primary()
.on_click(cx.listener(|this, _, window, cx| {
this.connect(window, cx)
})),
),
),
)
}
}
pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
let presets = app.read(cx).aliyun_presets.clone();
let dialog_view = cx.new(|cx| AliyunDialog::new(app, presets, window, cx));
window.open_dialog(cx, move |dialog, _, _| {
dialog
.w(px(760.))
.title("阿里云 IoT 设备")
.child(dialog_view.clone())
.footer(gpui_kit::div())
});
}