From 07ff7094295b04f21e9e8982d606da461ca37093 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 17 Jan 2025 08:02:55 -0800 Subject: [PATCH] JWT auth plugin and necessary updates --- packages/plugin-runtime-types/package.json | 2 +- .../src/bindings/events.ts | 30 +++++++- packages/plugin-runtime/src/index.worker.ts | 8 ++- src-tauri/src/http_request.rs | 14 ++-- src-tauri/src/lib.rs | 21 ++---- src-tauri/src/template_callback.rs | 5 +- .../plugins/auth-basic/build/index.js | 7 +- .../plugins/auth-bearer/build/index.js | 7 +- .../vendored/plugins/auth-jwt/build/index.js | 56 ++++++++++++--- src-tauri/yaak-grpc/Cargo.toml | 2 +- src-tauri/yaak-plugins/bindings/events.ts | 30 +++++++- src-tauri/yaak-plugins/src/error.rs | 3 + src-tauri/yaak-plugins/src/events.rs | 41 ++++++++++- src-tauri/yaak-plugins/src/manager.rs | 48 ++++++++++--- src-web/components/DynamicForm.tsx | 71 +++++++++++++++++-- .../components/GrpcConnectionSetupPane.tsx | 5 +- .../components/HttpAuthenticationEditor.tsx | 2 +- src-web/components/RequestPane.tsx | 5 +- src-web/components/core/Editor/Editor.tsx | 38 ++++------ src-web/components/core/Input.tsx | 7 +- src-web/components/core/RadioDropdown.tsx | 1 - src-web/components/core/Tabs/Tabs.tsx | 2 +- src-web/hooks/useHttpAuthentication.ts | 17 +++-- src-web/lib/toast.ts | 9 +++ 24 files changed, 327 insertions(+), 104 deletions(-) diff --git a/packages/plugin-runtime-types/package.json b/packages/plugin-runtime-types/package.json index d8950448..dca8f622 100644 --- a/packages/plugin-runtime-types/package.json +++ b/packages/plugin-runtime-types/package.json @@ -1,6 +1,6 @@ { "name": "@yaakapp/api", - "version": "0.2.26", + "version": "0.2.27", "main": "lib/index.js", "typings": "./lib/index.d.ts", "files": [ diff --git a/packages/plugin-runtime-types/src/bindings/events.ts b/packages/plugin-runtime-types/src/bindings/events.ts index de27461c..d2fcb2e2 100644 --- a/packages/plugin-runtime-types/src/bindings/events.ts +++ b/packages/plugin-runtime-types/src/bindings/events.ts @@ -29,6 +29,8 @@ export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "s export type CopyTextRequest = { text: string, }; +export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown"; + export type EmptyPayload = {}; export type ExportHttpRequestRequest = { httpRequest: HttpRequest, }; @@ -49,7 +51,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, }; export type FindHttpResponsesResponse = { httpResponses: Array, }; -export type FormInput = { "type": "text" } & FormInputText | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest; +export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest; export type FormInputBase = { name: string, /** @@ -79,6 +81,24 @@ label?: string, */ defaultValue?: string, }; +export type FormInputEditor = { +/** + * Placeholder for the text input + */ +placeholder?: string | null, language: EditorLanguage, name: string, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * The default value + */ +defaultValue?: string, }; + export type FormInputFile = { /** * The title of the file selection window @@ -139,7 +159,11 @@ export type FormInputText = { /** * Placeholder for the text input */ -placeholder?: string | null, name: string, +placeholder?: string | null, +/** + * Placeholder for the text input + */ +password?: boolean, name: string, /** * Whether the user must fill in the argument */ @@ -153,7 +177,7 @@ label?: string, */ defaultValue?: string, }; -export type GetHttpAuthenticationResponse = { name: string, pluginName: string, config: Array, }; +export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array, }; export type GetHttpRequestActionsRequest = Record; diff --git a/packages/plugin-runtime/src/index.worker.ts b/packages/plugin-runtime/src/index.worker.ts index f49ec2cf..2a7a860b 100644 --- a/packages/plugin-runtime/src/index.worker.ts +++ b/packages/plugin-runtime/src/index.worker.ts @@ -304,11 +304,13 @@ async function initialize() { } if (payload.type === 'get_http_authentication_request' && mod.plugin?.authentication) { + const auth = mod.plugin.authentication; const replyPayload: InternalEventPayload = { + ...auth, type: 'get_http_authentication_response', - name: mod.plugin.authentication.name, - pluginName: pkg.name, - config: mod.plugin.authentication.config, + + // Remove unneeded attrs + onApply: undefined, }; sendPayload(windowContext, replyPayload, replyId); return; diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index b9550413..ff594e46 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -369,14 +369,8 @@ pub async fn send_http_request( }; // Apply authentication - - // Map legacy auth name values from before they were plugins - let auth_plugin_name = match request.authentication_type.clone() { - Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()), - Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()), - _ => request.authentication_type.to_owned(), - }; - if let Some(plugin_name) = auth_plugin_name { + + if let Some(auth_name) = request.authentication_type.to_owned() { let req = CallHttpAuthenticationRequest { config: serde_json::to_value(&request.authentication) .unwrap() @@ -395,13 +389,13 @@ pub async fn send_http_request( .collect(), }; let plugin_result = - match plugin_manager.call_http_authentication(window, &plugin_name, req).await { + match plugin_manager.call_http_authentication(window, &auth_name, req).await { Ok(r) => r, Err(e) => { return Ok(response_err(&*response.lock().await, e.to_string(), window).await); } }; - + { let url = sendable_req.url_mut(); *url = Url::parse(&plugin_result.url).unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 83285af5..342fe2fe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -235,19 +235,10 @@ async fn cmd_grpc_go( metadata.insert(h.name, h.value); } - // Map legacy auth name values from before they were plugins - let auth_plugin_name = match req.authentication_type.clone() { - Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()), - Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()), - _ => req.authentication_type.to_owned(), - }; - if let Some(plugin_name) = auth_plugin_name { + if let Some(auth_name) = req.authentication_type.clone() { + let auth = req.authentication.clone(); let plugin_req = CallHttpAuthenticationRequest { - config: serde_json::to_value(&req.authentication) - .unwrap() - .as_object() - .unwrap() - .to_owned(), + config: serde_json::to_value(&auth).unwrap().as_object().unwrap().to_owned(), method: "POST".to_string(), url: req.url.clone(), headers: metadata @@ -259,7 +250,7 @@ async fn cmd_grpc_go( .collect(), }; let plugin_result = plugin_manager - .call_http_authentication(&window, &plugin_name, plugin_req) + .call_http_authentication(&window, &auth_name, plugin_req) .await .map_err(|e| e.to_string())?; @@ -980,7 +971,9 @@ async fn cmd_get_http_authentication( window: WebviewWindow, plugin_manager: State<'_, PluginManager>, ) -> Result, String> { - plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string()) + let results = + plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())?; + Ok(results.into_iter().map(|(_, a)| a).collect()) } #[tauri::command] diff --git a/src-tauri/src/template_callback.rs b/src-tauri/src/template_callback.rs index 837578ef..4f9179cc 100644 --- a/src-tauri/src/template_callback.rs +++ b/src-tauri/src/template_callback.rs @@ -46,9 +46,10 @@ impl TemplateCallback for PluginTemplateCallback { let mut args_with_defaults = args.clone(); // Fill in default values for all args - for a_def in function.args { - let base = match a_def { + for arg in function.args { + let base = match arg { FormInput::Text(a) => a.base, + FormInput::Editor(a) => a.base, FormInput::Select(a) => a.base, FormInput::Checkbox(a) => a.base, FormInput::File(a) => a.base, diff --git a/src-tauri/vendored/plugins/auth-basic/build/index.js b/src-tauri/vendored/plugins/auth-basic/build/index.js index 9c603389..8d252474 100644 --- a/src-tauri/vendored/plugins/auth-basic/build/index.js +++ b/src-tauri/vendored/plugins/auth-basic/build/index.js @@ -25,7 +25,9 @@ __export(src_exports, { module.exports = __toCommonJS(src_exports); var plugin = { authentication: { - name: "Basic", + name: "basic", + label: "Basic Auth", + shortLabel: "Basic", config: [{ type: "text", name: "username", @@ -35,7 +37,8 @@ var plugin = { type: "text", name: "password", label: "Password", - optional: true + optional: true, + password: true }], async onApply(_ctx, args) { const { username, password } = args.config; diff --git a/src-tauri/vendored/plugins/auth-bearer/build/index.js b/src-tauri/vendored/plugins/auth-bearer/build/index.js index 8eb0089f..af82ce10 100644 --- a/src-tauri/vendored/plugins/auth-bearer/build/index.js +++ b/src-tauri/vendored/plugins/auth-bearer/build/index.js @@ -25,12 +25,15 @@ __export(src_exports, { module.exports = __toCommonJS(src_exports); var plugin = { authentication: { - name: "Bearer", + name: "bearer", + label: "Bearer Token", + shortLabel: "Bearer", config: [{ type: "text", name: "token", label: "Token", - optional: true + optional: true, + password: true }], async onApply(_ctx, args) { const { token } = args.config; diff --git a/src-tauri/vendored/plugins/auth-jwt/build/index.js b/src-tauri/vendored/plugins/auth-jwt/build/index.js index 2b0a2edc..8ab954a2 100644 --- a/src-tauri/vendored/plugins/auth-jwt/build/index.js +++ b/src-tauri/vendored/plugins/auth-jwt/build/index.js @@ -3793,17 +3793,57 @@ __export(src_exports, { }); module.exports = __toCommonJS(src_exports); var import_jsonwebtoken = __toESM(require_jsonwebtoken()); +var algorithms = [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512" +]; +var defaultAlgorithm = algorithms[0]; var plugin = { authentication: { - name: "JWT", - config: [{ - type: "text", - name: "foo", - label: "Foo", - optional: true - }], + name: "jwt", + label: "JWT Bearer", + shortLabel: "JWT", + config: [ + { + type: "select", + name: "algorithm", + label: "Algorithm", + defaultValue: defaultAlgorithm, + options: algorithms.map((value) => ({ name: value, value })) + }, + { + type: "text", + name: "secret", + label: "Secret", + optional: true + }, + { + type: "checkbox", + name: "secretBase64", + label: "Secret Base64 Encoded" + }, + { + type: "editor", + name: "payload", + label: "Payload", + language: "json", + optional: true + } + ], async onApply(_ctx, args) { - const token = import_jsonwebtoken.default.sign({ foo: "bar" }, null); + const { algorithm, secret: _secret, secretBase64, payload } = args.config; + const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`; + const token = import_jsonwebtoken.default.sign(`${payload}`, secret, { algorithm }); return { url: args.url, headers: [{ name: "Authorization", value: `Bearer ${token}` }] diff --git a/src-tauri/yaak-grpc/Cargo.toml b/src-tauri/yaak-grpc/Cargo.toml index 1725b2f0..2722bad2 100644 --- a/src-tauri/yaak-grpc/Cargo.toml +++ b/src-tauri/yaak-grpc/Cargo.toml @@ -13,7 +13,7 @@ tokio-stream = "0.1.14" prost-types = "0.13.4" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" -prost-reflect = { version = "0.14.4", features = ["serde", "derive"] } +prost-reflect = { version = "0.14.4", default-features = false, features = ["serde", "derive"] } log = "0.4.20" anyhow = "1.0.79" hyper = "1.5.2" diff --git a/src-tauri/yaak-plugins/bindings/events.ts b/src-tauri/yaak-plugins/bindings/events.ts index de27461c..d2fcb2e2 100644 --- a/src-tauri/yaak-plugins/bindings/events.ts +++ b/src-tauri/yaak-plugins/bindings/events.ts @@ -29,6 +29,8 @@ export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "s export type CopyTextRequest = { text: string, }; +export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown"; + export type EmptyPayload = {}; export type ExportHttpRequestRequest = { httpRequest: HttpRequest, }; @@ -49,7 +51,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, }; export type FindHttpResponsesResponse = { httpResponses: Array, }; -export type FormInput = { "type": "text" } & FormInputText | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest; +export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest; export type FormInputBase = { name: string, /** @@ -79,6 +81,24 @@ label?: string, */ defaultValue?: string, }; +export type FormInputEditor = { +/** + * Placeholder for the text input + */ +placeholder?: string | null, language: EditorLanguage, name: string, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * The default value + */ +defaultValue?: string, }; + export type FormInputFile = { /** * The title of the file selection window @@ -139,7 +159,11 @@ export type FormInputText = { /** * Placeholder for the text input */ -placeholder?: string | null, name: string, +placeholder?: string | null, +/** + * Placeholder for the text input + */ +password?: boolean, name: string, /** * Whether the user must fill in the argument */ @@ -153,7 +177,7 @@ label?: string, */ defaultValue?: string, }; -export type GetHttpAuthenticationResponse = { name: string, pluginName: string, config: Array, }; +export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array, }; export type GetHttpRequestActionsRequest = Record; diff --git a/src-tauri/yaak-plugins/src/error.rs b/src-tauri/yaak-plugins/src/error.rs index 2940502f..d6bba76b 100644 --- a/src-tauri/yaak-plugins/src/error.rs +++ b/src-tauri/yaak-plugins/src/error.rs @@ -25,6 +25,9 @@ pub enum Error { #[error("Plugin not found: {0}")] PluginNotFoundErr(String), + + #[error("Auth plugin not found: {0}")] + AuthPluginNotFound(String), #[error("Plugin error: {0}")] PluginErr(String), diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index f24462b5..72e586a9 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -298,7 +298,8 @@ pub enum Icon { #[ts(export, export_to = "events.ts")] pub struct GetHttpAuthenticationResponse { pub name: String, - pub plugin_name: String, + pub label: String, + pub short_label: String, pub config: Vec, } @@ -356,6 +357,7 @@ pub struct TemplateFunction { #[ts(export, export_to = "events.ts")] pub enum FormInput { Text(FormInputText), + Editor(FormInputEditor), Select(FormInputSelect), Checkbox(FormInputCheckbox), File(FormInputFile), @@ -391,6 +393,43 @@ pub struct FormInputText { /// Placeholder for the text input #[ts(optional = nullable)] pub placeholder: Option, + + /// Placeholder for the text input + #[ts(optional)] + pub password: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export, export_to = "events.ts")] +pub enum EditorLanguage { + Text, + Javascript, + Json, + Html, + Xml, + Graphql, + Markdown, +} + +impl Default for EditorLanguage { + fn default() -> Self { + Self::Text + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "events.ts")] +pub struct FormInputEditor { + #[serde(flatten)] + pub base: FormInputBase, + + /// Placeholder for the text input + #[ts(optional = nullable)] + pub placeholder: Option, + + pub language: EditorLanguage, } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 5a25b648..42bfad94 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -1,9 +1,11 @@ -use crate::error::Error::{ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr}; +use crate::error::Error::{ + AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr, +}; use crate::error::Result; use crate::events::{ BootRequest, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionRequest, - CallTemplateFunctionResponse, EmptyPayload, FilterRequest, FilterResponse, + CallTemplateFunctionResponse, EmptyPayload, FilterRequest, FilterResponse, FormInput, GetHttpAuthenticationResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose, WindowContext, @@ -462,7 +464,7 @@ impl PluginManager { pub async fn get_http_authentication( &self, window: &WebviewWindow, - ) -> Result> { + ) -> Result> { let window_context = WindowContext::from_window(window); let reply_events = self .send_and_wait( @@ -474,7 +476,11 @@ impl PluginManager { let mut results = Vec::new(); for event in reply_events { if let InternalEventPayload::GetHttpAuthenticationResponse(resp) = event.payload { - results.push(resp.clone()); + let plugin = self + .get_plugin_by_ref_id(&event.plugin_ref_id) + .await + .ok_or(PluginNotFoundErr(event.plugin_ref_id))?; + results.push((plugin, resp.clone())); } } @@ -484,13 +490,37 @@ impl PluginManager { pub async fn call_http_authentication( &self, window: &WebviewWindow, - plugin_name: &str, + auth_name: &str, req: CallHttpAuthenticationRequest, ) -> Result { - let plugin = self - .get_plugin_by_name(plugin_name) - .await - .ok_or(PluginNotFoundErr(plugin_name.to_string()))?; + let handlers = self.get_http_authentication(window).await?; + let (plugin, authentication) = handlers + .iter() + .find(|(_, a)| a.name == auth_name) + .ok_or(AuthPluginNotFound(auth_name.to_string()))?; + + // Clone for mutability + let mut req = req.clone(); + + // Fill in default values + for arg in authentication.config.clone() { + let base = match arg { + FormInput::Text(a) => a.base, + FormInput::Editor(a) => a.base, + FormInput::Select(a) => a.base, + FormInput::Checkbox(a) => a.base, + FormInput::File(a) => a.base, + FormInput::HttpRequest(a) => a.base, + }; + if let None = req.config.get(base.name.as_str()) { + let default = match base.default_value { + None => serde_json::Value::Null, + Some(s) => serde_json::Value::String(s), + }; + req.config.insert(base.name, default); + } + } + let event = self .send_to_plugin_and_wait( WindowContext::from_window(window), diff --git a/src-web/components/DynamicForm.tsx b/src-web/components/DynamicForm.tsx index 92c1edfb..c31de568 100644 --- a/src-web/components/DynamicForm.tsx +++ b/src-web/components/DynamicForm.tsx @@ -2,19 +2,24 @@ import type { Folder, HttpRequest } from '@yaakapp-internal/models'; import type { FormInput, FormInputCheckbox, + FormInputEditor, FormInputFile, FormInputHttpRequest, FormInputSelect, FormInputText, } from '@yaakapp-internal/plugins'; +import classNames from 'classnames'; import { useCallback } from 'react'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useFolders } from '../hooks/useFolders'; import { useHttpRequests } from '../hooks/useHttpRequests'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { Checkbox } from './core/Checkbox'; +import { Editor } from './core/Editor/Editor'; import { Input } from './core/Input'; +import { Label } from './core/Label'; import { Select } from './core/Select'; +import { VStack } from './core/Stacks'; import { SelectFile } from './SelectFile'; // eslint-disable-next-line react-refresh/only-export-components @@ -41,7 +46,7 @@ export function DynamicForm>({ ); return ( -
+ {config.map((a, i) => { switch (a.type) { case 'select': @@ -50,7 +55,7 @@ export function DynamicForm>({ key={i + stateKey} arg={a} onChange={(v) => setDataAttr(a.name, v)} - value={data[a.name] ? String(data[a.name]) : '__ERROR__'} + value={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG} /> ); case 'text': @@ -64,6 +69,17 @@ export function DynamicForm>({ value={data[a.name] ? String(data[a.name]) : ''} /> ); + case 'editor': + return ( + setDataAttr(a.name, v)} + value={data[a.name] ? String(data[a.name]) : ''} + /> + ); case 'checkbox': return ( >({ ); } })} -
+ ); } @@ -123,10 +139,11 @@ function TextArg({ onChange={handleChange} defaultValue={value === DYNAMIC_FORM_NULL_ARG ? '' : value} require={!arg.optional} + type={arg.password ? 'password' : 'text'} label={ <> {arg.label ?? arg.name} - {arg.optional && (optional)} + {arg.optional && (optional)} } hideLabel={arg.label == null} @@ -138,6 +155,50 @@ function TextArg({ ); } +function EditorArg({ + arg, + onChange, + value, + useTemplating, + stateKey, +}: { + arg: FormInputEditor; + value: string; + onChange: (v: string) => void; + useTemplating: boolean; + stateKey: string; +}) { + const handleChange = useCallback( + (value: string) => { + onChange(value === '' ? DYNAMIC_FORM_NULL_ARG : value); + }, + [onChange], + ); + + const id = `input-${arg.name}`; + + return ( +
+ + +
+ ); +} + function SelectArg({ arg, value, @@ -155,7 +216,7 @@ function SelectArg({ value={value} options={[ ...arg.options.map((a) => ({ - label: a.name + (arg.defaultValue === a.value ? ' (default)' : ''), + label: a.name, value: a.value === arg.defaultValue ? DYNAMIC_FORM_NULL_ARG : a.value, })), ]} diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index 45d480bd..970803eb 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -143,8 +143,9 @@ export function GrpcConnectionSetupPane({ value: activeRequest.authenticationType, items: [ ...authentication.map((a) => ({ - label: a.name, - value: a.pluginName, + label: a.label || 'UNKNOWN', + shortLabel: a.shortLabel, + value: a.name, })), { type: 'separator' }, { label: 'No Authentication', shortLabel: 'Auth', value: null }, diff --git a/src-web/components/HttpAuthenticationEditor.tsx b/src-web/components/HttpAuthenticationEditor.tsx index fb772af5..6a345c3d 100644 --- a/src-web/components/HttpAuthenticationEditor.tsx +++ b/src-web/components/HttpAuthenticationEditor.tsx @@ -14,7 +14,7 @@ export function HttpAuthenticationEditor({ request }: Props) { const updateHttpRequest = useUpdateAnyHttpRequest(); const updateGrpcRequest = useUpdateAnyGrpcRequest(); const auths = useHttpAuthentication(); - const auth = auths.find((a) => a.pluginName === request.authenticationType); + const auth = auths.find((a) => a.name === request.authenticationType); const handleChange = useCallback( (authentication: Record) => { diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index cbf33247..2df53315 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -235,8 +235,9 @@ export const RequestPane = memo(function RequestPane({ value: activeRequest.authenticationType, items: [ ...authentication.map((a) => ({ - label: a.name, - value: a.pluginName, + label: a.label || 'UNKNOWN', + shortLabel: a.shortLabel, + value: a.name, })), { type: 'separator' }, { label: 'No Authentication', shortLabel: 'Auth', value: null }, diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 2025eb69..8b85f9df 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -7,7 +7,7 @@ import { emacs } from '@replit/codemirror-emacs'; import { vim } from '@replit/codemirror-vim'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models'; -import type { TemplateFunction } from '@yaakapp-internal/plugins'; +import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; import { EditorView } from 'codemirror'; import type { MutableRefObject, ReactNode } from 'react'; @@ -51,16 +51,7 @@ export interface EditorProps { type?: 'text' | 'password'; className?: string; heightMode?: 'auto' | 'full'; - language?: - | 'javascript' - | 'json' - | 'html' - | 'xml' - | 'graphql' - | 'url' - | 'pairs' - | 'text' - | 'markdown'; + language?: EditorLanguage | 'pairs'; forceUpdateKey?: string | number; autoFocus?: boolean; autoSelect?: boolean; @@ -90,10 +81,6 @@ const stateFields = { history: historyField, folds: foldState }; const emptyVariables: EnvironmentVariable[] = []; const emptyExtension: Extension = []; -// NOTE: For some reason, the cursor doesn't appear if the field is empty and there is no -// placeholder. So we set it to a space to force it to show. -const emptyPlaceholder = ' '; - export const Editor = forwardRef(function Editor( { readOnly, @@ -178,11 +165,11 @@ export const Editor = forwardRef(function E useEffect( function configurePlaceholder() { if (cm.current === null) return; - const ext = placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder)); + const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type)); const effect = placeholderCompartment.current.reconfigure(ext); cm.current?.view.dispatch({ effects: effect }); }, - [placeholder], + [placeholder, type], ); // Update vim @@ -354,7 +341,7 @@ export const Editor = forwardRef(function E const extensions = [ languageCompartment.of(langExt), placeholderCompartment.current.of( - placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder)), + placeholderExt(placeholderElFromText(placeholder ?? '', type)), ), wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : []), keymapCompartment.current.of( @@ -592,18 +579,21 @@ function getExtensions({ ]; } -const placeholderElFromText = (text: string) => { +const placeholderElFromText = (text: string, type: EditorProps['type']) => { const el = document.createElement('div'); - el.innerHTML = text.replaceAll('\n', '
'); + if (type === 'password') { + // Will be obscured (dots) so just needs to be something to take up space + el.innerHTML = 'aaaaaaaaaa'; + el.setAttribute('aria-hidden', 'true'); + } else { + el.innerHTML = text ? text.replaceAll('\n', '
') : ' '; + } return el; }; function saveCachedEditorState(stateKey: string | null, state: EditorState | null) { if (!stateKey || state == null) return; - sessionStorage.setItem( - computeFullStateKey(stateKey), - JSON.stringify(state.toJSON(stateFields)), - ); + sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(state.toJSON(stateFields))); } function getCachedEditorState(doc: string, stateKey: string | null) { diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 02f99500..00f542d7 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -3,6 +3,7 @@ import type { EditorView } from 'codemirror'; import type { ReactNode } from 'react'; import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { useStateWithDeps } from '../../hooks/useStateWithDeps'; +import { generateId } from '../../lib/generateId'; import type { EditorProps } from './Editor/Editor'; import { Editor } from './Editor/Editor'; import { IconButton } from './IconButton'; @@ -94,7 +95,7 @@ export const Input = forwardRef(function Input( onBlur?.(); }, [onBlur]); - const id = `input-${label}`; + const id = useRef(`input-${generateId()}`); const editorClassName = classNames( className, '!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder', @@ -140,7 +141,7 @@ export const Input = forwardRef(function Input( labelPosition === 'top' && 'flex-row gap-0.5', )} > -