mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-23 19:15:00 +01:00
Compare commits
7 Commits
v2025.2.0-
...
v2025.2.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5070513ac | ||
|
|
f5c3798df9 | ||
|
|
469d12fede | ||
|
|
417a02744b | ||
|
|
81e78ef24c | ||
|
|
dad9cebb9e | ||
|
|
b3ede3d6d6 |
@@ -123,7 +123,12 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
match settings.proxy {
|
||||
Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(),
|
||||
Some(ProxySetting::Enabled { http, https, auth }) => {
|
||||
Some(ProxySetting::Enabled {
|
||||
http,
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
}) if !disabled => {
|
||||
debug!("Using proxy http={http} https={https}");
|
||||
let mut proxy = Proxy::custom(move |url| {
|
||||
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
|
||||
@@ -143,7 +148,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
None => {} // Nothing to do for this one, as it is the default
|
||||
_ => {} // Nothing to do for this one, as it is the default
|
||||
}
|
||||
|
||||
// Add cookie store if specified
|
||||
|
||||
@@ -54,7 +54,7 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
|
||||
|
||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
|
||||
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
import type { Environment } from "./gen_models";
|
||||
import type { Folder } from "./gen_models";
|
||||
import type { GrpcRequest } from "./gen_models";
|
||||
import type { HttpRequest } from "./gen_models";
|
||||
import type { WebsocketRequest } from "./gen_models";
|
||||
import type { Workspace } from "./gen_models";
|
||||
|
||||
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
||||
|
||||
@@ -31,6 +31,9 @@ macro_rules! impl_model {
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub enum ProxySetting {
|
||||
Enabled {
|
||||
#[serde(default)]
|
||||
// This was added after on so give it a default to be able to deserialize older values
|
||||
disabled: bool,
|
||||
http: String,
|
||||
https: String,
|
||||
auth: Option<ProxySettingAuth>,
|
||||
|
||||
@@ -11,6 +11,11 @@ impl<'a> DbContext<'a> {
|
||||
let (sql, params) = Query::select()
|
||||
.from(KeyValueIden::Table)
|
||||
.column(Asterisk)
|
||||
// Temporary clause to prevent bug when reverting to the previous version, before the
|
||||
// ID column was added. A previous version will not know about ID and will create
|
||||
// key/value entries that don't have one. This clause ensures they are not queried
|
||||
// TODO: Add migration to delete key/values with NULL IDs later on, then remove this
|
||||
.cond_where(Expr::col(KeyValueIden::Id).is_not_null())
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = self.conn.prepare(sql.as_str())?;
|
||||
let items = stmt.query_map(&*params.as_params(), KeyValue::from_row)?;
|
||||
|
||||
@@ -5,15 +5,15 @@ use log::{info, warn};
|
||||
use std::str::FromStr;
|
||||
use tauri::http::{HeaderMap, HeaderName};
|
||||
use tauri::{AppHandle, Runtime, State, Url, WebviewWindow};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use yaak_http::apply_path_placeholders;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::models::{
|
||||
HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent,
|
||||
WebsocketEventType, WebsocketRequest,
|
||||
};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{
|
||||
CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose,
|
||||
@@ -257,12 +257,16 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
// Add URL parameters to URL
|
||||
let mut url = Url::parse(&url).unwrap();
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in url_parameters.clone() {
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
let valid_query_pairs = url_parameters
|
||||
.into_iter()
|
||||
.filter(|p| p.enabled && !p.name.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
// NOTE: Only mutate query pairs if there are any, or it will append an empty `?` to the URL
|
||||
if !valid_query_pairs.is_empty() {
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in valid_query_pairs {
|
||||
query_pairs.append_pair(p.name.as_str(), p.value.as_str());
|
||||
}
|
||||
query_pairs.append_pair(p.name.as_str(), p.value.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useAtomValue } from 'jotai';
|
||||
import React from 'react';
|
||||
import { dialogsAtom, hideDialog } from '../lib/dialog';
|
||||
import { Dialog, type DialogProps } from './core/Dialog';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
export type DialogInstance = {
|
||||
id: string;
|
||||
@@ -22,16 +23,17 @@ export function Dialogs() {
|
||||
function DialogInstance({ render, onClose, id, ...props }: DialogInstance) {
|
||||
const children = render({ hide: () => hideDialog(id) });
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
key={id}
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
hideDialog(id);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
<ErrorBoundary name={`Dialog ${id}`}>
|
||||
<Dialog
|
||||
open
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
hideDialog(id);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
data={data}
|
||||
className="pb-4" // Pad the bottom to look nice
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -77,15 +78,17 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
setDataAttr,
|
||||
data,
|
||||
disabled,
|
||||
className,
|
||||
}: Pick<
|
||||
Props<T>,
|
||||
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
|
||||
> & {
|
||||
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<VStack space={3} className="h-full overflow-auto">
|
||||
<VStack space={3} className={classNames(className, 'h-full overflow-auto')}>
|
||||
{inputs?.map((input, i) => {
|
||||
if ('hidden' in input && input.hidden) {
|
||||
return null;
|
||||
|
||||
68
src-web/components/ErrorBoundary.tsx
Normal file
68
src-web/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { ErrorInfo, ReactNode } from 'react';
|
||||
import { Component, useEffect } from 'react';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import RouteError from './RouteError';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.warn('Error caught by ErrorBoundary:', error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Banner color="danger" className="flex items-center gap-2">
|
||||
<div>
|
||||
Error rendering <InlineCode>{this.props.name}</InlineCode> component
|
||||
</div>
|
||||
<Button
|
||||
className="inline-flex"
|
||||
variant="border"
|
||||
color="danger"
|
||||
size="2xs"
|
||||
onClick={() => {
|
||||
showDialog({
|
||||
id: 'error-boundary',
|
||||
render: () => <RouteError error={this.state.error} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show
|
||||
</Button>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorBoundaryTestThrow() {
|
||||
useEffect(() => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
@@ -92,27 +93,29 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
<AutoScroller
|
||||
data={events}
|
||||
header={
|
||||
activeConnection.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{activeConnection.error}
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
render={(event) => (
|
||||
<EventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
isActive={event.id === activeEventId}
|
||||
onClick={() => {
|
||||
if (event.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(event.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ErrorBoundary name="GRPC Events">
|
||||
<AutoScroller
|
||||
data={events}
|
||||
header={
|
||||
activeConnection.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{activeConnection.error}
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
render={(event) => (
|
||||
<EventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
isActive={event.id === activeEventId}
|
||||
onClick={() => {
|
||||
if (event.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(event.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { PdfViewer } from './responseViewers/PdfViewer';
|
||||
import { SvgViewer } from './responseViewers/SvgViewer';
|
||||
import { VideoViewer } from './responseViewers/VideoViewer';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -155,35 +156,37 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
tabListClassName="mt-1.5"
|
||||
>
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ConfirmLargeResponse response={activeResponse}>
|
||||
{activeResponse.state === 'initialized' ? (
|
||||
<EmptyStateText>
|
||||
<LoadingIcon size="xl" className="text-text-subtlest" />
|
||||
</EmptyStateText>
|
||||
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
|
||||
<EmptyStateText>Empty </EmptyStateText>
|
||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
||||
<EventStreamViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image\/svg/) ? (
|
||||
<SvgViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
|
||||
) : mimeType?.match(/^audio/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
|
||||
) : mimeType?.match(/^video/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
|
||||
) : mimeType?.match(/pdf/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
|
||||
) : mimeType?.match(/csv|tab-separated/i) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<HTMLOrTextViewer
|
||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === 'pretty'}
|
||||
/>
|
||||
)}
|
||||
</ConfirmLargeResponse>
|
||||
<ErrorBoundary name="Http Response Viewer">
|
||||
<ConfirmLargeResponse response={activeResponse}>
|
||||
{activeResponse.state === 'initialized' ? (
|
||||
<EmptyStateText>
|
||||
<LoadingIcon size="xl" className="text-text-subtlest" />
|
||||
</EmptyStateText>
|
||||
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
|
||||
<EmptyStateText>Empty </EmptyStateText>
|
||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
||||
<EventStreamViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image\/svg/) ? (
|
||||
<SvgViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
|
||||
) : mimeType?.match(/^audio/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
|
||||
) : mimeType?.match(/^video/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
|
||||
) : mimeType?.match(/pdf/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
|
||||
) : mimeType?.match(/csv|tab-separated/i) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<HTMLOrTextViewer
|
||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === 'pretty'}
|
||||
/>
|
||||
)}
|
||||
</ConfirmLargeResponse>
|
||||
</ErrorBoundary>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<ResponseHeaders response={activeResponse} />
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { Prose } from './Prose';
|
||||
|
||||
export function Markdown({ children, className }: { children: string; className?: string }) {
|
||||
return (
|
||||
<Prose className={className}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
<ErrorBoundary name="Markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</ErrorBoundary>
|
||||
</Prose>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormattedError } from './core/FormattedError';
|
||||
import { Heading } from './core/Heading';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export default function RouteError({ error }: { error: unknown; reset: () => void }) {
|
||||
export default function RouteError({ error }: { error: unknown }) {
|
||||
console.log('Error', error);
|
||||
const stringified = JSON.stringify(error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -24,6 +24,7 @@ export function SettingsProxy() {
|
||||
} else if (v === 'enabled') {
|
||||
await patchModel(settings, {
|
||||
proxy: {
|
||||
disabled: false,
|
||||
type: 'enabled',
|
||||
http: '',
|
||||
https: '',
|
||||
@@ -42,16 +43,42 @@ export function SettingsProxy() {
|
||||
/>
|
||||
{settings.proxy?.type === 'enabled' && (
|
||||
<VStack space={1.5}>
|
||||
<HStack space={1.5} className="mt-3">
|
||||
<Checkbox
|
||||
className="my-3"
|
||||
checked={!settings.proxy.disabled}
|
||||
title="Enable proxy"
|
||||
help="Use this to temporarily disable the proxy without losing the configuration"
|
||||
onChange={async (enabled) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = !enabled;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label="HTTP"
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.http}
|
||||
onChange={async (http) => {
|
||||
const https = settings.proxy?.type === 'enabled' ? settings.proxy.https : '';
|
||||
const auth = settings.proxy?.type === 'enabled' ? settings.proxy.auth : null;
|
||||
await patchModel(settings, { proxy: { type: 'enabled', http, https, auth } });
|
||||
const { proxy } = settings;
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
proxy: {
|
||||
type: 'enabled',
|
||||
http,
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PlainInput
|
||||
@@ -60,9 +87,13 @@ export function SettingsProxy() {
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.https}
|
||||
onChange={async (https) => {
|
||||
const http = settings.proxy?.type === 'enabled' ? settings.proxy.http : '';
|
||||
const auth = settings.proxy?.type === 'enabled' ? settings.proxy.auth : null;
|
||||
await patchModel(settings, { proxy: { type: 'enabled', http, https, auth } });
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
@@ -71,10 +102,14 @@ export function SettingsProxy() {
|
||||
checked={settings.proxy.auth != null}
|
||||
title="Enable authentication"
|
||||
onChange={async (enabled) => {
|
||||
const http = settings.proxy?.type === 'enabled' ? settings.proxy.http : '';
|
||||
const https = settings.proxy?.type === 'enabled' ? settings.proxy.https : '';
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const auth = enabled ? { user: '', password: '' } : null;
|
||||
await patchModel(settings, { proxy: { type: 'enabled', http, https, auth } });
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -87,12 +122,15 @@ export function SettingsProxy() {
|
||||
placeholder="myUser"
|
||||
defaultValue={settings.proxy.auth.user}
|
||||
onChange={async (user) => {
|
||||
const https = settings.proxy?.type === 'enabled' ? settings.proxy.https : '';
|
||||
const http = settings.proxy?.type === 'enabled' ? settings.proxy.http : '';
|
||||
const password =
|
||||
settings.proxy?.type === 'enabled' ? (settings.proxy.auth?.password ?? '') : '';
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, { proxy: { type: 'enabled', http, https, auth } });
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PlainInput
|
||||
@@ -102,12 +140,15 @@ export function SettingsProxy() {
|
||||
placeholder="s3cretPassw0rd"
|
||||
defaultValue={settings.proxy.auth.password}
|
||||
onChange={async (password) => {
|
||||
const https = settings.proxy?.type === 'enabled' ? settings.proxy.https : '';
|
||||
const http = settings.proxy?.type === 'enabled' ? settings.proxy.http : '';
|
||||
const user =
|
||||
settings.proxy?.type === 'enabled' ? (settings.proxy.auth?.user ?? '') : '';
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, { proxy: { type: 'enabled', http, https, auth } });
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { type ReactNode } from 'react';
|
||||
import { hideToast, toastsAtom } from '../lib/toast';
|
||||
import { Toast, type ToastProps } from './core/Toast';
|
||||
import { Portal } from './Portal';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
export type ToastInstance = {
|
||||
id: string;
|
||||
@@ -22,16 +23,17 @@ export const Toasts = () => {
|
||||
{toasts.map((toast: ToastInstance) => {
|
||||
const { message, uniqueKey, ...props } = toast;
|
||||
return (
|
||||
<Toast
|
||||
key={uniqueKey}
|
||||
open
|
||||
{...props}
|
||||
// We call onClose inside actions.hide instead of passing to toast so that
|
||||
// it gets called from external close calls as well
|
||||
onClose={() => hideToast(toast)}
|
||||
>
|
||||
{message}
|
||||
</Toast>
|
||||
<ErrorBoundary key={uniqueKey} name={`Toast ${uniqueKey}`}>
|
||||
<Toast
|
||||
open
|
||||
{...props}
|
||||
// We call onClose inside actions.hide instead of passing to toast so that
|
||||
// it gets called from external close calls as well
|
||||
onClose={() => hideToast(toast)}
|
||||
>
|
||||
{message}
|
||||
</Toast>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { WebsocketStatusTag } from './core/WebsocketStatusTag';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { RecentWebsocketConnectionsDropdown } from './RecentWebsocketConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
@@ -93,27 +94,29 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
<AutoScroller
|
||||
data={events}
|
||||
header={
|
||||
activeConnection.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{activeConnection.error}
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
render={(event) => (
|
||||
<EventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
isActive={event.id === activeEventId}
|
||||
onClick={() => {
|
||||
if (event.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(event.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ErrorBoundary name="Websocket Events">
|
||||
<AutoScroller
|
||||
data={events}
|
||||
header={
|
||||
activeConnection.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{activeConnection.error}
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
render={(event) => (
|
||||
<EventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
isActive={event.id === activeEventId}
|
||||
onClick={() => {
|
||||
if (event.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(event.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { Sidebar } from './sidebar/Sidebar';
|
||||
import { SidebarActions } from './sidebar/SidebarActions';
|
||||
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
const head = { gridArea: 'head' };
|
||||
@@ -149,13 +150,17 @@ export function Workspace() {
|
||||
<HeaderSize size="lg" className="border-transparent">
|
||||
<SidebarActions />
|
||||
</HeaderSize>
|
||||
<Sidebar />
|
||||
<ErrorBoundary name="Sidebar (Floating)">
|
||||
<Sidebar />
|
||||
</ErrorBoundary>
|
||||
</m.div>
|
||||
</Overlay>
|
||||
) : (
|
||||
<>
|
||||
<div style={side} className={classNames('x-theme-sidebar', 'overflow-hidden bg-surface')}>
|
||||
<Sidebar className="border-r border-border-subtle" />
|
||||
<ErrorBoundary name="Sidebar">
|
||||
<Sidebar className="border-r border-border-subtle" />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<ResizeHandle
|
||||
className="-translate-x-3"
|
||||
@@ -175,7 +180,9 @@ export function Workspace() {
|
||||
>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderSize>
|
||||
<WorkspaceBody />
|
||||
<ErrorBoundary name="Workspace Body">
|
||||
<WorkspaceBody />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import { Icon } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
import { Separator } from './Separator';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
@@ -202,17 +203,19 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
triggerRef={buttonRef}
|
||||
fullWidth={fullWidth}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
onClose={() => setIsOpen(false)}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
<ErrorBoundary name={`Dropdown Menu`}>
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
triggerRef={buttonRef}
|
||||
fullWidth={fullWidth}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
onClose={() => setIsOpen(false)}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'react';
|
||||
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { copyToClipboard } from '../../lib/copy';
|
||||
import {
|
||||
analyzeTemplate,
|
||||
convertTemplateToInsecure,
|
||||
@@ -30,7 +31,6 @@ import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
import { copyToClipboard } from '../../lib/copy';
|
||||
|
||||
export type InputProps = Pick<
|
||||
EditorProps,
|
||||
@@ -127,13 +127,29 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
|
||||
|
||||
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
|
||||
|
||||
const lastWindowFocus = useRef<number>(0);
|
||||
useEffect(() => {
|
||||
const fn = () => (lastWindowFocus.current = Date.now());
|
||||
window.addEventListener('focus', fn);
|
||||
return () => {
|
||||
window.removeEventListener('focus', fn);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
|
||||
// Select all text of input when it's focused to match standard browser behavior.
|
||||
// This should not, however, select when the input is focused due to a window focus event, so
|
||||
// we handle that case as well.
|
||||
const windowJustFocused = Date.now() - lastWindowFocus.current < 200;
|
||||
if (!windowJustFocused) {
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
||||
});
|
||||
}
|
||||
|
||||
setFocused(true);
|
||||
// Select all text on focus
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
||||
});
|
||||
onFocus?.();
|
||||
}, [onFocus, readOnly]);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Icon } from '../Icon';
|
||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
import { HStack } from '../Stacks';
|
||||
import { ErrorBoundary } from '../../ErrorBoundary';
|
||||
|
||||
export type TabItem =
|
||||
| {
|
||||
@@ -153,12 +154,14 @@ export const TabContent = memo(function TabContent({
|
||||
className,
|
||||
}: TabContentProps) {
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
data-tab={value}
|
||||
className={classNames(className, 'tab-content', 'hidden w-full h-full')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<ErrorBoundary name={`Tab ${value}`}>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
data-tab={value}
|
||||
className={classNames(className, 'tab-content', 'hidden w-full h-full')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,15 +5,13 @@ export function useWindowFocus() {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let unsub: undefined | (() => void) = undefined;
|
||||
getCurrentWebviewWindow()
|
||||
.onFocusChanged((e) => {
|
||||
setVisible(e.payload);
|
||||
})
|
||||
.then((fn) => {
|
||||
unsub = fn;
|
||||
});
|
||||
return () => unsub?.();
|
||||
const unlisten = getCurrentWebviewWindow().onFocusChanged((e) => {
|
||||
setVisible(e.payload);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return visible;
|
||||
|
||||
Reference in New Issue
Block a user