DB & Dev Tools · APIs: a log held at its newest, secrets per environment, moves
WebSocketConsole draws its log through chat::MessageList, so it holds at the newest message. EnvironmentSelector opens a secret for its own environment only. CollectionTree takes on_move and devtools::moved applies a drop; the gallery opens a saved request into the builder and moves them. Tests cover the builder's refill, Send at rest, Bearer sign-in, the indented body, the eye, a field's pick path and the log.
This commit is contained in:
1 parent
3c9f52f853
commit
658e4e73c6
9 files changed
+488
-137
No files matched your search
@@ -390,3 +390,4 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
| T33b | 1 | FAIL | YAML lists at their key's indent and quoted `#` misread; a long note hid the key at 280px; XML text paths counted from 0 and entities stayed raw; odd-key paths and Format at rest untested |
|
| T33b | 1 | FAIL | YAML lists at their key's indent and quoted `#` misread; a long note hid the key at 280px; XML text paths counted from 0 and entities stayed raw; odd-key paths and Format at rest untested |
|
||||||
| T33b | 2 | FAIL | quoted YAML scalars kept their escapes and single-quoted keys their quotes; the comment scanner's escape and after-a-space rules untested |
|
| T33b | 2 | FAIL | quoted YAML scalars kept their escapes and single-quoted keys their quotes; the comment scanner's escape and after-a-space rules untested |
|
||||||
| T33b | 3 | FAIL | five escape arms untested and a quoted key holding `: ` split inside its quotes; both fixed after the last round with tests that go red on each mutation, no fourth round |
|
| T33b | 3 | FAIL | five escape arms untested and a quoted key holding `: ` split inside its quotes; both fixed after the last round with tests that go red on each mutation, no fourth round |
|
||||||
|
| T33c | 1 | FAIL | the socket log stayed at its top; a revealed secret stayed open across environments; the collection drew drags it never moved; the builder's refill, Send's rest and Bearer, the indented body, the eye and a field's pick path untested |
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use ely_gpui_component::{
|
use ely_gpui_component::{
|
||||||
devtools::{
|
devtools::{
|
||||||
ApiRequest, ApiRequestBuilder, ApiResponse, CollectionTree, Direction, Environment,
|
ApiRequest, ApiRequestBuilder, ApiResponse, Auth, CollectionTree, Direction, Environment,
|
||||||
EnvironmentSelector, GraphQLExplorer, Method, ResponseViewer, Saved, SchemaType,
|
EnvironmentSelector, GraphQLExplorer, Method, ResponseViewer, Saved, SchemaType,
|
||||||
SocketMessage, SocketState, TypeKind, WebSocketConsole,
|
SocketMessage, SocketState, TypeKind, WebSocketConsole, moved,
|
||||||
},
|
},
|
||||||
editor::CodeEditor,
|
editor::CodeEditor,
|
||||||
forms::TextInput,
|
forms::TextInput,
|
||||||
@@ -16,43 +16,89 @@ use jiff::Timestamp;
|
|||||||
use crate::ui::{change, keep, section};
|
use crate::ui::{change, keep, section};
|
||||||
|
|
||||||
fn environments() -> Vec<Environment> {
|
fn environments() -> Vec<Environment> {
|
||||||
let env = |key: &str, name: &str, host: &str| Environment {
|
let env = |key: &str, name: &str, host: &str, token: &str| Environment {
|
||||||
key: key.to_string().into(),
|
key: key.to_string().into(),
|
||||||
name: name.to_string().into(),
|
name: name.to_string().into(),
|
||||||
variables: vec![
|
variables: vec![
|
||||||
("host".into(), host.to_string().into()),
|
("host".into(), host.to_string().into()),
|
||||||
("token".into(), "sk_live_4f9a2c".into()),
|
("token".into(), token.to_string().into()),
|
||||||
],
|
],
|
||||||
secrets: vec!["token".into()],
|
secrets: vec!["token".into()],
|
||||||
};
|
};
|
||||||
vec![
|
vec![
|
||||||
env("local", "Local", "localhost:8080"),
|
env("local", "Local", "localhost:8080", "sk_test_local"),
|
||||||
env("staging", "Staging", "staging.api.example.com"),
|
env(
|
||||||
env("production", "Production", "api.example.com"),
|
"staging",
|
||||||
|
"Staging",
|
||||||
|
"staging.api.example.com",
|
||||||
|
"sk_test_4f9a2c",
|
||||||
|
),
|
||||||
|
env(
|
||||||
|
"production",
|
||||||
|
"Production",
|
||||||
|
"api.example.com",
|
||||||
|
"sk_live_91c07e",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The API demo's request, its environment, the answer and the request picked.
|
/// The saved requests, each as the builder opens it.
|
||||||
|
fn saved_requests() -> Vec<ApiRequest> {
|
||||||
|
let mut list = ApiRequest::new("list-orders", Method::Get, "https://{{host}}/v1/orders");
|
||||||
|
list.params = vec![
|
||||||
|
("status".into(), "open".into()),
|
||||||
|
("limit".into(), "20".into()),
|
||||||
|
];
|
||||||
|
list.headers = vec![("Accept".into(), "application/json".into())];
|
||||||
|
let mut create = ApiRequest::new("create-order", Method::Post, "https://{{host}}/v1/orders");
|
||||||
|
create.body = r#"{"items": [{"sku": "tea", "quantity": 2}]}"#.into();
|
||||||
|
create.auth = Auth::Bearer("{{token}}".into());
|
||||||
|
let cancel = ApiRequest::new(
|
||||||
|
"cancel-order",
|
||||||
|
Method::Delete,
|
||||||
|
"https://{{host}}/v1/orders/1009",
|
||||||
|
);
|
||||||
|
let me = ApiRequest::new("me", Method::Get, "https://{{host}}/v1/me");
|
||||||
|
let mut rename = ApiRequest::new("rename", Method::Patch, "https://{{host}}/v1/me");
|
||||||
|
rename.body = r#"{"name": "Ada"}"#.into();
|
||||||
|
vec![list, create, cancel, me, rename]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The API demo's saved requests and their folders, the one open, its environment and the last answer.
|
||||||
struct Desk {
|
struct Desk {
|
||||||
request: ApiRequest,
|
requests: Vec<ApiRequest>,
|
||||||
|
collection: Vec<Saved>,
|
||||||
|
open: SharedString,
|
||||||
environment: SharedString,
|
environment: SharedString,
|
||||||
response: Option<ApiResponse>,
|
response: Option<ApiResponse>,
|
||||||
picked: Vec<SharedString>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Desk {
|
impl Desk {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut request = ApiRequest::new("list-orders", Method::Get, "https://{{host}}/v1/orders");
|
|
||||||
request.params = vec![
|
|
||||||
("status".into(), "open".into()),
|
|
||||||
("limit".into(), "20".into()),
|
|
||||||
];
|
|
||||||
request.headers = vec![("Accept".into(), "application/json".into())];
|
|
||||||
Self {
|
Self {
|
||||||
request,
|
requests: saved_requests(),
|
||||||
|
collection: vec![
|
||||||
|
Saved::folder(
|
||||||
|
"orders",
|
||||||
|
"Orders",
|
||||||
|
[
|
||||||
|
Saved::request("list-orders", "List orders", Method::Get),
|
||||||
|
Saved::request("create-order", "Create an order", Method::Post),
|
||||||
|
Saved::request("cancel-order", "Cancel an order", Method::Delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Saved::folder(
|
||||||
|
"users",
|
||||||
|
"Users",
|
||||||
|
[
|
||||||
|
Saved::request("me", "Who am I", Method::Get),
|
||||||
|
Saved::request("rename", "Rename", Method::Patch),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
open: "list-orders".into(),
|
||||||
environment: "staging".into(),
|
environment: "staging".into(),
|
||||||
response: Some(answer()),
|
response: Some(answer()),
|
||||||
picked: vec!["list-orders".into()],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,41 +120,28 @@ fn answer() -> ApiResponse {
|
|||||||
pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let desk = keep("devtools-api", Desk::new, window, cx);
|
let desk = keep("devtools-api", Desk::new, window, cx);
|
||||||
let now = desk.read(cx);
|
let now = desk.read(cx);
|
||||||
let (request, environment, response, picked) = (
|
let (collection, open, environment, response) = (
|
||||||
now.request.clone(),
|
now.collection.clone(),
|
||||||
|
now.open.clone(),
|
||||||
now.environment.clone(),
|
now.environment.clone(),
|
||||||
now.response.clone(),
|
now.response.clone(),
|
||||||
now.picked.clone(),
|
|
||||||
);
|
);
|
||||||
|
let request = now
|
||||||
|
.requests
|
||||||
|
.iter()
|
||||||
|
.find(|request| request.key == open)
|
||||||
|
.expect("the open request is saved")
|
||||||
|
.clone();
|
||||||
let variables = environments()
|
let variables = environments()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|each| each.key == environment)
|
.find(|each| each.key == environment)
|
||||||
.expect("a listed environment")
|
.expect("a listed environment")
|
||||||
.variables;
|
.variables;
|
||||||
let [sent, chose, opened] = [(); 3].map(|_| desk.clone());
|
let [sent, chose, opened, dragged] = [(); 4].map(|_| desk.clone());
|
||||||
let collection = [
|
|
||||||
Saved::folder(
|
|
||||||
"orders",
|
|
||||||
"Orders",
|
|
||||||
[
|
|
||||||
Saved::request("list-orders", "List orders", Method::Get),
|
|
||||||
Saved::request("create-order", "Create an order", Method::Post),
|
|
||||||
Saved::request("cancel-order", "Cancel an order", Method::Delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Saved::folder(
|
|
||||||
"users",
|
|
||||||
"Users",
|
|
||||||
[
|
|
||||||
Saved::request("me", "Who am I", Method::Get),
|
|
||||||
Saved::request("rename", "Rename", Method::Patch),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
section(
|
section(
|
||||||
"ApiRequestBuilder · ResponseViewer · HeadersTable · QueryParamsEditor → forms::KeyValueInput · EnvironmentSelector · CollectionTree",
|
"ApiRequestBuilder · ResponseViewer · HeadersTable · QueryParamsEditor → forms::KeyValueInput · EnvironmentSelector · CollectionTree",
|
||||||
"Saved requests in folders; the one open, built with its method and address, its query's pairs, headers, body and sign-in, and the address that goes out with the environment's variables filled in. Send brings back an answer: its status, time and size, then its body or its headers.",
|
"Saved requests in folders; a drag moves one, and Enter opens it in the builder with its method and address, its query's pairs, headers, body and sign-in, and the address that goes out with the environment's variables filled in. Send brings back an answer: its status, time and size, then its body or its headers.",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -136,8 +169,17 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.child(
|
.child(
|
||||||
CollectionTree::new("devtools-collection", collection)
|
CollectionTree::new("devtools-collection", collection)
|
||||||
.open(["orders"])
|
.open(["orders"])
|
||||||
.selected(picked)
|
.selected([open])
|
||||||
.on_open(move |key, _, cx| change(&opened, cx, |desk| desk.picked = vec![key.clone()])),
|
.on_open(move |key, _, cx| {
|
||||||
|
change(&opened, cx, |desk| {
|
||||||
|
if desk.requests.iter().any(|request| request.key == *key) {
|
||||||
|
desk.open = key.clone();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_move(move |key, target, at, _, cx| {
|
||||||
|
change(&dragged, cx, |desk| desk.collection = moved(&desk.collection, key, target, at))
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -147,11 +189,16 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(ApiRequestBuilder::new("devtools-request", request).variables(variables).on_send(
|
||||||
ApiRequestBuilder::new("devtools-request", request)
|
move |request, _, cx| {
|
||||||
.variables(variables)
|
change(&sent, cx, |desk| {
|
||||||
.on_send(move |_, _, cx| change(&sent, cx, |desk| desk.response = Some(answer()))),
|
if let Some(saved) = desk.requests.iter_mut().find(|each| each.key == request.key) {
|
||||||
)
|
*saved = request;
|
||||||
|
}
|
||||||
|
desk.response = Some(answer());
|
||||||
|
})
|
||||||
|
},
|
||||||
|
))
|
||||||
.children(response.map(|response| ResponseViewer::new("devtools-response", response))),
|
.children(response.map(|response| ResponseViewer::new("devtools-response", response))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+122
-2
@@ -4,7 +4,7 @@ use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Styled, Window
|
|||||||
|
|
||||||
use super::request::Method;
|
use super::request::Method;
|
||||||
use crate::{
|
use crate::{
|
||||||
lists::{Tree, TreeNode},
|
lists::{DropAt, Tree, TreeNode},
|
||||||
primitives::IconName,
|
primitives::IconName,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +48,12 @@ impl Saved {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn key(&self) -> &SharedString {
|
||||||
|
match self {
|
||||||
|
Saved::Folder { key, .. } | Saved::Request { key, .. } => key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn node(&self) -> TreeNode {
|
fn node(&self) -> TreeNode {
|
||||||
match self {
|
match self {
|
||||||
Saved::Folder { key, name, items } => TreeNode::new(key.clone(), name.clone())
|
Saved::Folder { key, name, items } => TreeNode::new(key.clone(), name.clone())
|
||||||
@@ -60,10 +66,58 @@ impl Saved {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Takes `key` out of `items`, wherever it lies.
|
||||||
|
fn taken(items: &mut Vec<Saved>, key: &str) -> Option<Saved> {
|
||||||
|
if let Some(ix) = items.iter().position(|item| item.key() == key) {
|
||||||
|
return Some(items.remove(ix));
|
||||||
|
}
|
||||||
|
items.iter_mut().find_map(|item| match item {
|
||||||
|
Saved::Folder { items, .. } => taken(items, key),
|
||||||
|
Saved::Request { .. } => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets `item` at `at` of `target`, or hands it back when `target` is not in `items`.
|
||||||
|
fn placed(items: &mut Vec<Saved>, item: Saved, target: &str, at: DropAt) -> Result<(), Saved> {
|
||||||
|
if let Some(ix) = items.iter().position(|each| each.key() == target) {
|
||||||
|
match (at, &mut items[ix]) {
|
||||||
|
(DropAt::Before, _) => items.insert(ix, item),
|
||||||
|
(DropAt::After, _) => items.insert(ix + 1, item),
|
||||||
|
(DropAt::Inside, Saved::Folder { items, .. }) => items.push(item),
|
||||||
|
(DropAt::Inside, Saved::Request { key, .. }) => {
|
||||||
|
panic!("collection: {key} is a request, and nothing goes inside it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut item = item;
|
||||||
|
for each in items.iter_mut() {
|
||||||
|
if let Saved::Folder { items, .. } = each {
|
||||||
|
match placed(items, item, target, at) {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(back) => item = back,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `items` with `key` moved to `at` of `target`, as a drop in the tree asks.
|
||||||
|
pub fn moved(items: &[Saved], key: &str, target: &str, at: DropAt) -> Vec<Saved> {
|
||||||
|
let mut items = items.to_vec();
|
||||||
|
let item = taken(&mut items, key).unwrap_or_else(|| panic!("collection: no {key} to move"));
|
||||||
|
if let Err(item) = placed(&mut items, item, target, at) {
|
||||||
|
panic!("collection: no {target} to move {} beside", item.key());
|
||||||
|
}
|
||||||
|
log::info!("collection: moved {key} {at:?} {target}");
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
||||||
type OnKeys = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
|
type OnKeys = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
|
||||||
|
type OnMove = Rc<dyn Fn(&SharedString, &SharedString, DropAt, &mut Window, &mut App)>;
|
||||||
|
|
||||||
/// Saved requests in folders, each with its method. Enter or a double press opens a request; a drag moves one into another folder or among its own. It fills its box, which the host sizes.
|
/// Saved requests in folders, each with its method. Enter or a double press opens a request; with `on_move`, a drag moves one into another folder or among its own, and `moved` applies it. It fills its box, which the host sizes.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct CollectionTree {
|
pub struct CollectionTree {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -72,6 +126,7 @@ pub struct CollectionTree {
|
|||||||
selected: Vec<SharedString>,
|
selected: Vec<SharedString>,
|
||||||
on_open: Option<OnKey>,
|
on_open: Option<OnKey>,
|
||||||
on_select: Option<OnKeys>,
|
on_select: Option<OnKeys>,
|
||||||
|
on_move: Option<OnMove>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionTree {
|
impl CollectionTree {
|
||||||
@@ -83,6 +138,7 @@ impl CollectionTree {
|
|||||||
selected: Vec::new(),
|
selected: Vec::new(),
|
||||||
on_open: None,
|
on_open: None,
|
||||||
on_select: None,
|
on_select: None,
|
||||||
|
on_move: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +162,15 @@ impl CollectionTree {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs with the request or folder dragged, the row it landed on, and where on that row.
|
||||||
|
pub fn on_move(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&SharedString, &SharedString, DropAt, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_move = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_select(
|
pub fn on_select(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
|
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
|
||||||
@@ -130,6 +195,61 @@ impl RenderOnce for CollectionTree {
|
|||||||
if let Some(on_select) = self.on_select {
|
if let Some(on_select) = self.on_select {
|
||||||
tree = tree.on_select(move |keys, window, cx| on_select(keys, window, cx));
|
tree = tree.on_select(move |keys, window, cx| on_select(keys, window, cx));
|
||||||
}
|
}
|
||||||
|
if let Some(on_move) = self.on_move {
|
||||||
|
tree = tree
|
||||||
|
.on_move(move |key, target, at, window, cx| on_move(key, target, at, window, cx));
|
||||||
|
}
|
||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Saved, moved};
|
||||||
|
use crate::{devtools::Method, lists::DropAt};
|
||||||
|
|
||||||
|
fn keys(items: &[Saved]) -> Vec<String> {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.flat_map(|item| match item {
|
||||||
|
Saved::Folder { key, items, .. } => {
|
||||||
|
let inner = keys(items).join(",");
|
||||||
|
vec![format!("{key}[{inner}]")]
|
||||||
|
}
|
||||||
|
Saved::Request { key, .. } => vec![key.to_string()],
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_drop_moves_a_request_beside_a_row_or_into_a_folder() {
|
||||||
|
let items = vec![
|
||||||
|
Saved::folder(
|
||||||
|
"a",
|
||||||
|
"A",
|
||||||
|
[
|
||||||
|
Saved::request("x", "X", Method::Get),
|
||||||
|
Saved::request("y", "Y", Method::Get),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Saved::folder("b", "B", []),
|
||||||
|
Saved::request("z", "Z", Method::Post),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
keys(&moved(&items, "x", "b", DropAt::Inside)),
|
||||||
|
["a[y]", "b[x]", "z"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keys(&moved(&items, "z", "x", DropAt::Before)),
|
||||||
|
["a[z,x,y]", "b[]"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keys(&moved(&items, "x", "z", DropAt::After)),
|
||||||
|
["a[y]", "b[]", "z", "x"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keys(&moved(&items, "b", "a", DropAt::Before)),
|
||||||
|
["b[]", "a[x,y]", "z"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-15
@@ -1,8 +1,8 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
Window, div,
|
SharedString, Styled, Window, div,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -64,13 +64,16 @@ fn shown_value(value: &SharedString, secret: bool, open: bool) -> SharedString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens `name` when it is shut, shuts it when it is open.
|
/// A secret by its environment's key and its name.
|
||||||
fn flipped(open: &mut Vec<SharedString>, name: &SharedString) {
|
type Secret = (SharedString, SharedString);
|
||||||
match open.iter().position(|each| each == name) {
|
|
||||||
|
/// Opens `secret` when it is shut, shuts it when it is open.
|
||||||
|
fn flipped(open: &mut Vec<Secret>, secret: &Secret) {
|
||||||
|
match open.iter().position(|each| each == secret) {
|
||||||
Some(ix) => {
|
Some(ix) => {
|
||||||
open.remove(ix);
|
open.remove(ix);
|
||||||
}
|
}
|
||||||
None => open.push(name.clone()),
|
None => open.push(secret.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,16 +91,21 @@ impl RenderOnce for EnvironmentSelector {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.clone();
|
.clone();
|
||||||
let shown =
|
let shown = window.use_keyed_state((id.clone(), "shown"), cx, |_, _| Vec::<Secret>::new());
|
||||||
window.use_keyed_state((id.clone(), "shown"), cx, |_, _| Vec::<SharedString>::new());
|
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let on_change = self
|
let on_change = self
|
||||||
.on_change
|
.on_change
|
||||||
.unwrap_or_else(|| panic!("environment selector {id:?} has no on_change"));
|
.unwrap_or_else(|| panic!("environment selector {id:?} has no on_change"));
|
||||||
let rows = chosen.variables.iter().map(|(name, value)| {
|
let rows = chosen.variables.iter().map(|(name, value)| {
|
||||||
let secret = chosen.secrets.contains(name);
|
let secret = chosen.secrets.contains(name);
|
||||||
let open = shown.read(cx).contains(name);
|
let key: Secret = (chosen.key.clone(), name.clone());
|
||||||
let (flip, name_key) = (shown.clone(), name.clone());
|
let open = shown.read(cx).contains(&key);
|
||||||
|
let flip = shown.clone();
|
||||||
|
let seen = format!(
|
||||||
|
"environment-{}-{name}-{}",
|
||||||
|
chosen.key,
|
||||||
|
if secret && !open { "masked" } else { "shown" }
|
||||||
|
);
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -117,6 +125,7 @@ impl RenderOnce for EnvironmentSelector {
|
|||||||
.min_w_0()
|
.min_w_0()
|
||||||
.text_size(theme.text_size(TextSize::Sm))
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
.text_color(theme.colors.fg_muted)
|
.text_color(theme.colors.fg_muted)
|
||||||
|
.debug_selector(move || seen)
|
||||||
.child(Ellipsis::new(shown_value(value, secret, open))),
|
.child(Ellipsis::new(shown_value(value, secret, open))),
|
||||||
)
|
)
|
||||||
.children(secret.then(|| {
|
.children(secret.then(|| {
|
||||||
@@ -135,7 +144,7 @@ impl RenderOnce for EnvironmentSelector {
|
|||||||
})
|
})
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
flip.update(cx, |shown, cx| {
|
flip.update(cx, |shown, cx| {
|
||||||
flipped(shown, &name_key);
|
flipped(shown, &key);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -181,10 +190,11 @@ mod tests {
|
|||||||
assert_eq!(shown_value(&value, true, false), "••••••••");
|
assert_eq!(shown_value(&value, true, false), "••••••••");
|
||||||
assert_eq!(shown_value(&value, true, true), "s3cret");
|
assert_eq!(shown_value(&value, true, true), "s3cret");
|
||||||
assert_eq!(shown_value(&value, false, false), "s3cret");
|
assert_eq!(shown_value(&value, false, false), "s3cret");
|
||||||
let (mut open, name) = (Vec::new(), SharedString::from("token"));
|
let secret = (SharedString::from("staging"), SharedString::from("token"));
|
||||||
flipped(&mut open, &name);
|
let mut open = Vec::new();
|
||||||
assert_eq!(open, std::slice::from_ref(&name));
|
flipped(&mut open, &secret);
|
||||||
flipped(&mut open, &name);
|
assert_eq!(open, std::slice::from_ref(&secret));
|
||||||
|
flipped(&mut open, &secret);
|
||||||
assert!(open.is_empty(), "a second press shuts it again");
|
assert!(open.is_empty(), "a second press shuts it again");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@ mod xml;
|
|||||||
#[cfg(all(test, feature = "test-support"))]
|
#[cfg(all(test, feature = "test-support"))]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use collection::{CollectionTree, Saved};
|
pub use collection::{CollectionTree, Saved, moved};
|
||||||
pub use connection_form::{ConnectionForm, TestState};
|
pub use connection_form::{ConnectionForm, TestState};
|
||||||
pub use connections::{Connection, ConnectionManager, ConnectionState, Engine};
|
pub use connections::{Connection, ConnectionManager, ConnectionState, Engine};
|
||||||
pub use datum::{Datum, Unread, read_json, read_yaml};
|
pub use datum::{Datum, Unread, read_json, read_yaml};
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ impl ApiRequestBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the request as written, `{{names}}` and all; `resolve` fills them from the environment.
|
||||||
pub fn on_send(
|
pub fn on_send(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(ApiRequest, &mut Window, &mut App) + 'static,
|
handler: impl Fn(ApiRequest, &mut Window, &mut App) + 'static,
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ impl RenderOnce for HeadersTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A body as the viewer shows it: JSON indented, anything else as it came.
|
||||||
|
fn shown_body(body: &str) -> String {
|
||||||
|
reformat(body, true).unwrap_or_else(|| body.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// An answer to a request: its status in its tone with the standard words, how long it took and how big it is; then its body, JSON indented, or its headers.
|
/// An answer to a request: its status in its tone with the standard words, how long it took and how big it is; then its body, JSON indented, or its headers.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ResponseViewer {
|
pub struct ResponseViewer {
|
||||||
@@ -109,7 +114,7 @@ impl RenderOnce for ResponseViewer {
|
|||||||
});
|
});
|
||||||
let shown = tab.read(cx).clone();
|
let shown = tab.read(cx).clone();
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let body = reformat(&response.body, true).unwrap_or_else(|| response.body.to_string());
|
let body = shown_body(&response.body);
|
||||||
let panel = match shown.as_ref() {
|
let panel = match shown.as_ref() {
|
||||||
"headers" => HeadersTable::new(response.headers.clone()).into_any_element(),
|
"headers" => HeadersTable::new(response.headers.clone()).into_any_element(),
|
||||||
_ => div()
|
_ => div()
|
||||||
@@ -169,6 +174,7 @@ impl RenderOnce for ResponseViewer {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::shown_body;
|
||||||
use super::{reason, status_tone};
|
use super::{reason, status_tone};
|
||||||
use crate::data_display::Tone;
|
use crate::data_display::Tone;
|
||||||
|
|
||||||
@@ -182,4 +188,10 @@ mod tests {
|
|||||||
assert_eq!(status_tone(503), Tone::Danger);
|
assert_eq!(status_tone(503), Tone::Danger);
|
||||||
assert_eq!(reason(299), "");
|
assert_eq!(reason(299), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_json_body_shows_indented_and_other_text_as_it_came() {
|
||||||
|
assert_eq!(shown_body(r#"{"a":[1]}"#), "{\n \"a\": [\n 1\n ]\n}");
|
||||||
|
assert_eq!(shown_body("plain words"), "plain words");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+65
-54
@@ -1,14 +1,15 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window,
|
AnyElement, App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||||
div, prelude::*,
|
Styled, Window, div, prelude::*,
|
||||||
};
|
};
|
||||||
use jiff::Timestamp;
|
use jiff::Timestamp;
|
||||||
|
|
||||||
use super::json_editor::reformat;
|
use super::json_editor::reformat;
|
||||||
use crate::{
|
use crate::{
|
||||||
buttons::{Button, ButtonVariant},
|
buttons::{Button, ButtonVariant},
|
||||||
|
chat::MessageList,
|
||||||
data_display::{Badge, Tone},
|
data_display::{Badge, Tone},
|
||||||
forms::{Input, TextInput},
|
forms::{Input, TextInput},
|
||||||
primitives::{Icon, IconName},
|
primitives::{Icon, IconName},
|
||||||
@@ -94,6 +95,60 @@ impl WebSocketConsole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One message in the log: which way it went, its text, JSON indented, and when; the time drops below in a narrow box.
|
||||||
|
fn message_row(message: &SocketMessage, now: Timestamp, ix: usize, cx: &App) -> AnyElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let (icon, ink) = match message.direction {
|
||||||
|
Direction::Sent => (IconName::ArrowUpRight, theme.colors.accent),
|
||||||
|
Direction::Received => (IconName::ArrowDownLeft, theme.colors.success),
|
||||||
|
Direction::Note => (IconName::Info, theme.colors.fg_subtle),
|
||||||
|
};
|
||||||
|
let text = reformat(&message.text, true).unwrap_or_else(|| message.text.to_string());
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_2()
|
||||||
|
.py_1p5()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(theme.colors.border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.pt_0p5()
|
||||||
|
.child(Icon::new(icon).size(IconSize::Sm).color(ink)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_x_3()
|
||||||
|
.gap_y_0p5()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w(theme.label_width())
|
||||||
|
.font_family(theme.mono_family.clone())
|
||||||
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
|
.when(message.direction == Direction::Note, |line| {
|
||||||
|
line.text_color(theme.colors.fg_muted)
|
||||||
|
})
|
||||||
|
.child(text),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.text_size(theme.text_size(TextSize::Xs))
|
||||||
|
.text_color(theme.colors.fg_subtle)
|
||||||
|
.child(format::relative(message.at, now)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.w_full()
|
||||||
|
.debug_selector(move || format!("socket-message-{ix}"))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for WebSocketConsole {
|
impl RenderOnce for WebSocketConsole {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
@@ -103,54 +158,7 @@ impl RenderOnce for WebSocketConsole {
|
|||||||
SocketState::Open => (Tone::Success, "Open"),
|
SocketState::Open => (Tone::Success, "Open"),
|
||||||
};
|
};
|
||||||
let open = self.state == SocketState::Open;
|
let open = self.state == SocketState::Open;
|
||||||
let rows = self.messages.iter().map(|message| {
|
let (messages, now) = (Rc::new(self.messages), self.now);
|
||||||
let (icon, ink) = match message.direction {
|
|
||||||
Direction::Sent => (IconName::ArrowUpRight, theme.colors.accent),
|
|
||||||
Direction::Received => (IconName::ArrowDownLeft, theme.colors.success),
|
|
||||||
Direction::Note => (IconName::Info, theme.colors.fg_subtle),
|
|
||||||
};
|
|
||||||
let text = reformat(&message.text, true).unwrap_or_else(|| message.text.to_string());
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.py_1p5()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(theme.colors.border)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_none()
|
|
||||||
.pt_0p5()
|
|
||||||
.child(Icon::new(icon).size(IconSize::Sm).color(ink)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.flex()
|
|
||||||
.flex_wrap()
|
|
||||||
.gap_x_3()
|
|
||||||
.gap_y_0p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w(theme.label_width())
|
|
||||||
.font_family(theme.mono_family.clone())
|
|
||||||
.text_size(theme.text_size(TextSize::Sm))
|
|
||||||
.when(message.direction == Direction::Note, |line| {
|
|
||||||
line.text_color(theme.colors.fg_muted)
|
|
||||||
})
|
|
||||||
.child(text),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_none()
|
|
||||||
.text_size(theme.text_size(TextSize::Xs))
|
|
||||||
.text_color(theme.colors.fg_subtle)
|
|
||||||
.child(format::relative(message.at, self.now)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let on_send = self
|
let on_send = self
|
||||||
.on_send
|
.on_send
|
||||||
.unwrap_or_else(|| panic!("websocket console {:?} has no on_send", self.id));
|
.unwrap_or_else(|| panic!("websocket console {:?} has no on_send", self.id));
|
||||||
@@ -207,14 +215,17 @@ impl RenderOnce for WebSocketConsole {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id((self.id.clone(), "log"))
|
.debug_selector(|| "socket-log".into())
|
||||||
.max_h(theme.list_max_height())
|
.h(theme.list_max_height())
|
||||||
.overflow_y_scroll()
|
|
||||||
.px_3()
|
.px_3()
|
||||||
.rounded(theme.radius(Radius::Md))
|
.rounded(theme.radius(Radius::Md))
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(theme.colors.border)
|
.border_color(theme.colors.border)
|
||||||
.children(rows),
|
.child(MessageList::new(
|
||||||
|
(self.id.clone(), "log"),
|
||||||
|
messages.len(),
|
||||||
|
move |ix, _, cx| message_row(&messages[ix], now, ix, cx),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
+164
-15
@@ -1,42 +1,59 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement, Styled, TestAppContext,
|
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement, Styled, TestAppContext,
|
||||||
Window, div, px,
|
VisualTestContext, Window, div, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Bench, bench, said, say, settle, tab, tap};
|
use super::{Bench, bench, said, say, settle, tab, tap};
|
||||||
use crate::{
|
use crate::{
|
||||||
devtools::{
|
devtools::{
|
||||||
ApiRequest, ApiRequestBuilder, Auth, CollectionTree, GraphQLExplorer, Method, Saved,
|
ApiRequest, ApiRequestBuilder, Auth, CollectionTree, Direction, Environment,
|
||||||
SocketState, WebSocketConsole,
|
EnvironmentSelector, GraphQLExplorer, Method, Saved, SchemaType, SocketMessage,
|
||||||
|
SocketState, TypeKind, WebSocketConsole,
|
||||||
},
|
},
|
||||||
editor::CodeEditor,
|
editor::CodeEditor,
|
||||||
forms::TextInput,
|
forms::TextInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn builder(_: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
|
fn builder(bench: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
let mut request = ApiRequest::new("r", Method::Get, "https://{{host}}/items");
|
let mut request = ApiRequest::new("a", Method::Get, "https://{{host}}/items");
|
||||||
request.params = vec![("q".into(), "red shoes".into())];
|
request.params = vec![("q".into(), "red shoes".into())];
|
||||||
request.auth = Auth::Basic {
|
request.auth = Auth::Basic {
|
||||||
user: "ada".into(),
|
user: "ada".into(),
|
||||||
password: "pw".into(),
|
password: "pw".into(),
|
||||||
};
|
};
|
||||||
|
let mut sending = false;
|
||||||
|
match bench.choice {
|
||||||
|
1 => {
|
||||||
|
request = ApiRequest::new("b", Method::Post, "https://b.example.com/orders");
|
||||||
|
request.auth = Auth::Bearer("t0k".into());
|
||||||
|
}
|
||||||
|
2 => request = ApiRequest::new("c", Method::Get, ""),
|
||||||
|
3 => sending = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
ApiRequestBuilder::new("builder", request)
|
ApiRequestBuilder::new("builder", request)
|
||||||
.variables([("host", "api.example.com")])
|
.variables([("host", "api.example.com")])
|
||||||
|
.sending(sending)
|
||||||
.on_send(move |request, _, cx| {
|
.on_send(move |request, _, cx| {
|
||||||
say(
|
let words = format!(
|
||||||
&owner,
|
"{} {} {:?}",
|
||||||
format!(
|
request.method.name(),
|
||||||
"{} {} {:?}",
|
request.address(),
|
||||||
request.method.name(),
|
request.auth
|
||||||
request.address(),
|
);
|
||||||
request.auth
|
say(&owner, words, cx)
|
||||||
),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn choose(host: &Entity<Bench>, choice: usize, cx: &mut VisualTestContext) {
|
||||||
|
host.update(cx, |bench, cx| {
|
||||||
|
bench.choice = choice;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Stops: the method, the address, then Send.
|
/// Stops: the method, the address, then Send.
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn send_hands_on_the_request_with_its_pairs(cx: &mut TestAppContext) {
|
fn send_hands_on_the_request_with_its_pairs(cx: &mut TestAppContext) {
|
||||||
@@ -49,6 +66,32 @@ fn send_hands_on_the_request_with_its_pairs(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops: the method, the address, then Send.
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_new_request_refills_the_builder_with_its_sign_in(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(builder, cx);
|
||||||
|
choose(&host, 1, cx);
|
||||||
|
tab(3, cx);
|
||||||
|
tap("space", cx);
|
||||||
|
assert_eq!(
|
||||||
|
said(&host, cx),
|
||||||
|
[r#"POST https://b.example.com/orders Bearer("t0k")"#]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops: the method and the address; Send rests, so a third Tab wraps to the method.
|
||||||
|
#[gpui::test]
|
||||||
|
fn send_rests_without_an_address_and_while_sending(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(builder, cx);
|
||||||
|
for choice in [2, 3] {
|
||||||
|
choose(&host, choice, cx);
|
||||||
|
tab(3, cx);
|
||||||
|
tap("space", cx);
|
||||||
|
tap("escape", cx);
|
||||||
|
}
|
||||||
|
assert!(said(&host, cx).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
fn console(bench: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) -> AnyElement {
|
fn console(bench: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
let draft = window.use_keyed_state("draft", cx, TextInput::new);
|
let draft = window.use_keyed_state("draft", cx, TextInput::new);
|
||||||
let now: jiff::Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
|
let now: jiff::Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
|
||||||
@@ -165,3 +208,109 @@ fn run_waits_for_variables_that_read(cx: &mut TestAppContext) {
|
|||||||
tap("space", cx);
|
tap("space", cx);
|
||||||
assert_eq!(said(&host, cx), [r#"{ a } {"id": 1}"#]);
|
assert_eq!(said(&host, cx), [r#"{ a } {"id": 1}"#]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn environments(bench: &Bench, _: &mut Window, _: &mut App, _: Entity<Bench>) -> AnyElement {
|
||||||
|
let env = |key: &str, token: &str| Environment {
|
||||||
|
key: key.to_string().into(),
|
||||||
|
name: key.to_string().into(),
|
||||||
|
variables: vec![("token".into(), token.to_string().into())],
|
||||||
|
secrets: vec!["token".into()],
|
||||||
|
};
|
||||||
|
let selected = if bench.choice == 0 {
|
||||||
|
"staging"
|
||||||
|
} else {
|
||||||
|
"production"
|
||||||
|
};
|
||||||
|
EnvironmentSelector::new(
|
||||||
|
"environments",
|
||||||
|
[
|
||||||
|
env("staging", "sk_test_1111"),
|
||||||
|
env("production", "sk_live_9999"),
|
||||||
|
],
|
||||||
|
selected,
|
||||||
|
)
|
||||||
|
.on_change(|_, _, _| {})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops: the Select, then the token's eye.
|
||||||
|
#[gpui::test]
|
||||||
|
fn an_eye_opens_one_environments_secret_only(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(environments, cx);
|
||||||
|
tab(2, cx);
|
||||||
|
tap("space", cx);
|
||||||
|
assert!(cx.debug_bounds("environment-staging-token-shown").is_some());
|
||||||
|
choose(&host, 1, cx);
|
||||||
|
assert!(
|
||||||
|
cx.debug_bounds("environment-production-token-masked")
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cx.debug_bounds("environment-production-token-shown")
|
||||||
|
.is_none(),
|
||||||
|
"another environment's secret stays masked"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(_: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
|
let query = window.use_keyed_state("query", cx, |window, cx| {
|
||||||
|
CodeEditor::new("{ a }", window, cx)
|
||||||
|
});
|
||||||
|
let variables = window.use_keyed_state("variables", cx, |window, cx| {
|
||||||
|
CodeEditor::new("", window, cx)
|
||||||
|
});
|
||||||
|
let search = window.use_keyed_state("search", cx, TextInput::new);
|
||||||
|
let order = SchemaType {
|
||||||
|
name: "Order".into(),
|
||||||
|
kind: TypeKind::Object,
|
||||||
|
fields: vec![("id".into(), "ID!".into())],
|
||||||
|
};
|
||||||
|
GraphQLExplorer::new("graph", [order], &query, &variables, &search)
|
||||||
|
.on_run(|_, _, _, _| {})
|
||||||
|
.on_pick(move |path, _, cx| say(&owner, path.to_string(), cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops: the schema first, its cursor on Order.
|
||||||
|
#[gpui::test]
|
||||||
|
fn enter_on_a_field_hands_its_path(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(schema, cx);
|
||||||
|
tab(1, cx);
|
||||||
|
for key in ["right", "down", "enter"] {
|
||||||
|
tap(key, cx);
|
||||||
|
}
|
||||||
|
assert_eq!(said(&host, cx), ["Order.id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(_: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
|
let draft = window.use_keyed_state("draft", cx, TextInput::new);
|
||||||
|
let now: jiff::Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
|
||||||
|
let messages = (0..12).map(|ix| SocketMessage {
|
||||||
|
direction: Direction::Received,
|
||||||
|
text: format!("message {ix}").into(),
|
||||||
|
at: now,
|
||||||
|
});
|
||||||
|
WebSocketConsole::new(
|
||||||
|
"socket",
|
||||||
|
"wss://example.com",
|
||||||
|
SocketState::Open,
|
||||||
|
messages,
|
||||||
|
&draft,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
.on_send(move |text, _, cx| say(&owner, format!("send {text}"), cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn the_log_holds_at_its_newest_message(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = bench(log, cx);
|
||||||
|
let newest = cx
|
||||||
|
.debug_bounds("socket-message-11")
|
||||||
|
.expect("the newest message is drawn");
|
||||||
|
let log = cx.debug_bounds("socket-log").expect("the log");
|
||||||
|
assert!(
|
||||||
|
newest.bottom() <= log.bottom() && newest.top() >= log.top(),
|
||||||
|
"the newest message lies inside the log: {newest:?} in {log:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user