Environments in URL and better rendering

This commit is contained in:
Gregory Schier
2023-10-25 11:13:00 -07:00
parent 3b660ddbd0
commit 33c406ce49
44 changed files with 226 additions and 160 deletions

View File

@@ -7,35 +7,30 @@
#[macro_use] #[macro_use]
extern crate objc; extern crate objc;
use std::collections::HashMap; use crate::models::{find_environments, generate_id};
use std::env::current_dir;
use std::fs::{create_dir_all, File};
use std::io::Write;
use base64::Engine; use base64::Engine;
use http::header::{HeaderName, ACCEPT, USER_AGENT}; use http::header::{HeaderName, ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderValue, Method}; use http::{HeaderMap, HeaderValue, Method};
use rand::random; use rand::random;
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::{Json, JsonValue}; use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tauri::api::path::data_dir; use std::collections::HashMap;
use tauri::regex::Regex; use std::env::current_dir;
use std::fs::{create_dir_all, File};
use std::io::Write;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, Window, WindowUrl, Wry}; use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, Window, WindowUrl, Wry};
use tauri::{CustomMenuItem, Manager, WindowEvent}; use tauri::{CustomMenuItem, Manager, WindowEvent};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use window_ext::WindowExt; use window_ext::WindowExt;
use crate::models::{find_environments, generate_id};
mod models; mod models;
mod render;
mod window_ext; mod window_ext;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@@ -84,24 +79,13 @@ async fn actually_send_ephemeral_request(
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let mut url_string = request.url.to_string();
let environments = find_environments(&request.workspace_id, pool) let environments = find_environments(&request.workspace_id, pool)
.await .await
.expect("Failed to find environments"); .expect("Failed to find environments");
let environment: models::Environment = environments.first().unwrap().clone(); let environment: models::Environment = environments.first().unwrap().clone();
let variables = environment.data;
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex"); let mut url_string = render::render(&request.url, environment.clone());
url_string = re
.replace(&url_string, |caps: &tauri::regex::Captures| {
let key = caps.get(1).unwrap().as_str();
match variables.get(key) {
Some(v) => v.as_str().unwrap(),
None => "",
}
})
.to_string();
if !url_string.starts_with("http://") && !url_string.starts_with("https://") { if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string); url_string = format!("http://{}", url_string);

25
src-tauri/src/render.rs Normal file
View File

@@ -0,0 +1,25 @@
use tauri::regex::Regex;
use crate::models::Environment;
pub fn render(template: &str, environment: Environment) -> String {
let variables = environment.data;
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex");
let rendered = re
.replace(template, |caps: &tauri::regex::Captures| {
let key = caps.get(1).unwrap().as_str();
match variables.get(key) {
Some(v) => {
if v.is_string() {
v.as_str().expect("Should be string").to_string()
} else {
v.to_string()
}
}
None => "".to_string(),
}
})
.to_string();
rendered
}

View File

@@ -7,6 +7,7 @@ import RouteError from './RouteError';
import Workspace from './Workspace'; import Workspace from './Workspace';
import Workspaces from './Workspaces'; import Workspaces from './Workspaces';
import { DialogProvider } from './DialogContext'; import { DialogProvider } from './DialogContext';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -23,12 +24,16 @@ const router = createBrowserRouter([
element: <Workspaces />, element: <Workspaces />,
}, },
{ {
path: routePaths.workspace({ workspaceId: ':workspaceId' }), path: routePaths.workspace({
workspaceId: ':workspaceId',
environmentId: ':environmentId',
}),
element: <WorkspaceOrRedirect />, element: <WorkspaceOrRedirect />,
}, },
{ {
path: routePaths.request({ path: routePaths.request({
workspaceId: ':workspaceId', workspaceId: ':workspaceId',
environmentId: ':environmentId',
requestId: ':requestId', requestId: ':requestId',
}), }),
element: <Workspace />, element: <Workspace />,
@@ -42,18 +47,23 @@ export function AppRouter() {
} }
function WorkspaceOrRedirect() { function WorkspaceOrRedirect() {
const environmentId = useActiveEnvironmentId();
const recentRequests = useRecentRequests(); const recentRequests = useRecentRequests();
const requests = useRequests(); const requests = useRequests();
const request = requests.find((r) => r.id === recentRequests[0]); const request = requests.find((r) => r.id === recentRequests[0]);
const routes = useAppRoutes(); const routes = useAppRoutes();
if (request === undefined) { if (request === undefined || environmentId === null) {
return <Workspace />; return <Workspace />;
} }
return ( return (
<Navigate <Navigate
to={routes.paths.request({ workspaceId: request.workspaceId, requestId: request.id })} to={routes.paths.request({
workspaceId: request.workspaceId,
environmentId,
requestId: request.id,
})}
/> />
); );
} }

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import React, { memo } from 'react'; import React, { memo } from 'react';
interface Props { interface Props {
@@ -9,7 +9,7 @@ export const DropMarker = memo(
function DropMarker({ className }: Props) { function DropMarker({ className }: Props) {
return ( return (
<div <div
className={classnames( className={classNames(
className, className,
'relative w-full h-0 overflow-visible pointer-events-none', 'relative w-full h-0 overflow-visible pointer-events-none',
)} )}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
@@ -12,6 +12,7 @@ import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog'; import { EnvironmentEditDialog } from './EnvironmentEditDialog';
import { useAppRoutes } from '../hooks/useAppRoutes';
type Props = { type Props = {
className?: string; className?: string;
@@ -21,11 +22,12 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
className, className,
}: Props) { }: Props) {
const environments = useEnvironments(); const environments = useEnvironments();
const [activeEnvironment, setActiveEnvironment] = useActiveEnvironment(); const activeEnvironment = useActiveEnvironment();
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null); const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
const createEnvironment = useCreateEnvironment(); const createEnvironment = useCreateEnvironment();
const prompt = usePrompt(); const prompt = usePrompt();
const dialog = useDialog(); const dialog = useDialog();
const routes = useAppRoutes();
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
const environmentItems = environments.map( const environmentItems = environments.map(
@@ -33,7 +35,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
key: e.id, key: e.id,
label: e.name, label: e.name,
onSelect: async () => { onSelect: async () => {
setActiveEnvironment(e); routes.setEnvironment(e);
}, },
}), }),
[], [],
@@ -112,15 +114,15 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
dialog, dialog,
environments, environments,
prompt, prompt,
setActiveEnvironment,
updateEnvironment, updateEnvironment,
routes,
]); ]);
return ( return (
<Dropdown items={items}> <Dropdown items={items}>
<Button <Button
size="sm" size="sm"
className={classnames(className, 'text-gray-800 !px-2 truncate')} className={classNames(className, 'text-gray-800 !px-2 truncate')}
forDropdown forDropdown
> >
{activeEnvironment?.name ?? <span className="italic text-gray-500">No Environment</span>} {activeEnvironment?.name ?? <span className="italic text-gray-500">No Environment</span>}

View File

@@ -5,14 +5,17 @@ import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import type { Environment } from '../lib/models'; import type { Environment } from '../lib/models';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Editor } from './core/Editor'; import { Editor } from './core/Editor';
import classnames from 'classnames'; import classNames from 'classnames';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { Link } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes';
export const EnvironmentEditDialog = function() { export const EnvironmentEditDialog = function() {
const routes = useAppRoutes();
const prompt = usePrompt(); const prompt = usePrompt();
const environments = useEnvironments(); const environments = useEnvironments();
const createEnvironment = useCreateEnvironment(); const createEnvironment = useCreateEnvironment();
const [activeEnvironment, setActiveEnvironment] = useActiveEnvironment(); const activeEnvironment = useActiveEnvironment();
return ( return (
<div className="h-full grid gap-3 grid-cols-[auto_minmax(0,1fr)]"> <div className="h-full grid gap-3 grid-cols-[auto_minmax(0,1fr)]">
@@ -20,14 +23,14 @@ export const EnvironmentEditDialog = function() {
{environments.map((e) => ( {environments.map((e) => (
<Button <Button
size="sm" size="sm"
className={classnames( className={classNames(
'w-full', 'w-full',
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000', activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000',
)} )}
justify="start" justify="start"
key={e.id} key={e.id}
onClick={() => { onClick={() => {
setActiveEnvironment(e); routes.setEnvironment(e);
}} }}
> >
{e.name} {e.name}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -26,7 +26,7 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
{open && ( {open && (
<FocusTrap> <FocusTrap>
<motion.div <motion.div
className={classnames('fixed inset-0', zIndexes[zIndex])} className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
> >

View File

@@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window'; import { appWindow } from '@tauri-apps/api/window';
import classnames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
@@ -152,7 +152,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
return ( return (
<div <div
style={style} style={style}
className={classnames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')} className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
> >
{activeRequest && ( {activeRequest && (
<> <>

View File

@@ -1,5 +1,5 @@
import useResizeObserver from '@react-hook/resize-observer'; import useResizeObserver from '@react-hook/resize-observer';
import classnames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
@@ -120,7 +120,7 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
<ResizeHandle <ResizeHandle
style={drag} style={drag}
isResizing={isResizing} isResizing={isResizing}
className={classnames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')} className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')}
onResizeStart={handleResizeStart} onResizeStart={handleResizeStart}
onReset={handleReset} onReset={handleReset}
side={vertical ? 'top' : 'left'} side={vertical ? 'top' : 'left'}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React from 'react'; import React from 'react';
@@ -28,7 +28,7 @@ export function ResizeHandle({
aria-hidden aria-hidden
draggable draggable
style={style} style={style}
className={classnames( className={classNames(
className, className,
'group z-10 flex', 'group z-10 flex',
vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize', vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize',
@@ -45,7 +45,7 @@ export function ResizeHandle({
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */} {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
{isResizing && ( {isResizing && (
<div <div
className={classnames( className={classNames(
'fixed -left-20 -right-20 -top-20 -bottom-20', 'fixed -left-20 -right-20 -top-20 -bottom-20',
vertical && 'cursor-row-resize', vertical && 'cursor-row-resize',
!vertical && 'cursor-col-resize', !vertical && 'cursor-col-resize',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
@@ -14,7 +14,7 @@ export function ResponseHeaders({ headers }: Props) {
<HStack <HStack
space={3} space={3}
key={i} key={i}
className={classnames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')} className={classNames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')}
> >
<dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd> <dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd>
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt> <dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useCallback, memo, useEffect, useMemo, useState } from 'react'; import { useCallback, memo, useEffect, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
@@ -84,7 +84,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
return ( return (
<div <div
style={style} style={style}
className={classnames( className={classNames(
className, className,
'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1', 'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
'dark:bg-gray-100 rounded-md border border-highlight', 'dark:bg-gray-100 rounded-md border border-highlight',
@@ -96,7 +96,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
<> <>
<HStack <HStack
alignItems="center" alignItems="center"
className={classnames( className={classNames(
'text-gray-700 text-sm w-full flex-shrink-0', 'text-gray-700 text-sm w-full flex-shrink-0',
// Remove a bit of space because the tabs have lots too // Remove a bit of space because the tabs have lots too
'-mb-1.5', '-mb-1.5',

View File

@@ -7,13 +7,14 @@ import { VStack } from './core/Stacks';
export default function RouteError() { export default function RouteError() {
const error = useRouteError(); const error = useRouteError();
console.log("Error", error);
const stringified = JSON.stringify(error); const stringified = JSON.stringify(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error as any).message ?? stringified; const message = (error as any).message ?? stringified;
const routes = useAppRoutes(); const routes = useAppRoutes();
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="max-w-[30rem] !h-auto"> <VStack space={5} className="max-w-[50rem] !h-auto">
<Heading>Route Error 🔥</Heading> <Heading>Route Error 🔥</Heading>
<FormattedError>{message}</FormattedError> <FormattedError>{message}</FormattedError>
<VStack space={2}> <VStack space={2}>

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd'; import type { XYCoord } from 'react-dnd';
@@ -19,6 +19,7 @@ import { Icon } from './core/Icon';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
interface Props { interface Props {
className?: string; className?: string;
@@ -32,6 +33,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden(); const { hidden } = useSidebarHidden();
const sidebarRef = useRef<HTMLDivElement>(null); const sidebarRef = useRef<HTMLDivElement>(null);
const activeRequestId = useActiveRequestId(); const activeRequestId = useActiveRequestId();
const activeEnvironmentId = useActiveEnvironmentId();
const unorderedRequests = useRequests(); const unorderedRequests = useRequests();
const deleteAnyRequest = useDeleteAnyRequest(); const deleteAnyRequest = useDeleteAnyRequest();
const routes = useAppRoutes(); const routes = useAppRoutes();
@@ -58,11 +60,15 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const index = requests.findIndex((r) => r.id === requestId); const index = requests.findIndex((r) => r.id === requestId);
const request = requests[index]; const request = requests[index];
if (!request) return; if (!request) return;
routes.navigate('request', { requestId, workspaceId: request.workspaceId }); routes.navigate('request', {
requestId,
workspaceId: request.workspaceId,
environmentId: activeEnvironmentId,
});
setSelectedIndex(index); setSelectedIndex(index);
focusActiveRequest(index); focusActiveRequest(index);
}, },
[focusActiveRequest, requests, routes], [focusActiveRequest, requests, routes, activeEnvironmentId],
); );
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
@@ -143,7 +149,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
tabIndex={hidden ? -1 : 0} tabIndex={hidden ? -1 : 0}
className={classnames(className, 'h-full relative grid grid-rows-[minmax(0,1fr)_auto]')} className={classNames(className, 'h-full relative grid grid-rows-[minmax(0,1fr)_auto]')}
> >
<VStack <VStack
as="ul" as="ul"
@@ -299,7 +305,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
}, [onSelect, requestId]); }, [onSelect, requestId]);
return ( return (
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}> <li ref={ref} className={classNames(className, 'block group/item px-2 pb-0.5')}>
<button <button
// tabIndex={-1} // Will prevent drag-n-drop // tabIndex={-1} // Will prevent drag-n-drop
onClick={handleSelect} onClick={handleSelect}
@@ -307,7 +313,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
onDoubleClick={handleStartEditing} onDoubleClick={handleStartEditing}
data-active={isActive} data-active={isActive}
data-selected={selected} data-selected={selected}
className={classnames( className={classNames(
'w-full flex items-center text-sm h-xs px-2 rounded-md transition-colors', 'w-full flex items-center text-sm h-xs px-2 rounded-md transition-colors',
editing && 'ring-1 focus-within:ring-focus', editing && 'ring-1 focus-within:ring-focus',
isActive && 'bg-highlight text-gray-800', isActive && 'bg-highlight text-gray-800',
@@ -324,7 +330,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
/> />
) : ( ) : (
<span className={classnames('truncate', !requestName && 'text-gray-400 italic')}> <span className={classNames('truncate', !requestName && 'text-gray-400 italic')}>
{requestName || 'New Request'} {requestName || 'New Request'}
</span> </span>
)} )}
@@ -396,7 +402,7 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
<SidebarItem <SidebarItem
ref={ref} ref={ref}
draggable draggable
className={classnames(isDragging && 'opacity-20')} className={classNames(isDragging && 'opacity-20')}
requestName={requestName} requestName={requestName}
requestId={requestId} requestId={requestId}
{...props} {...props}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
@@ -44,7 +44,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
}); });
return ( return (
<form onSubmit={handleSubmit} className={classnames('url-bar', className)}> <form onSubmit={handleSubmit} className={classNames('url-bar', className)}>
<Input <Input
ref={inputRef} ref={inputRef}
size="sm" size="sm"

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { import type {
CSSProperties, CSSProperties,
@@ -113,7 +113,7 @@ export default function Workspace() {
return ( return (
<div <div
style={styles} style={styles}
className={classnames( className={classNames(
'grid w-full h-full', 'grid w-full h-full',
// Animate sidebar width changes but only when not resizing // Animate sidebar width changes but only when not resizing
// because it's too slow to animate on mouse move // because it's too slow to animate on mouse move
@@ -125,7 +125,7 @@ export default function Workspace() {
<motion.div <motion.div
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
className={classnames( className={classNames(
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]', 'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
'grid grid-rows-[auto_1fr]', 'grid grid-rows-[auto_1fr]',
)} )}
@@ -140,7 +140,7 @@ export default function Workspace() {
</Overlay> </Overlay>
) : ( ) : (
<> <>
<div style={side} className={classnames('overflow-hidden bg-gray-100')}> <div style={side} className={classNames('overflow-hidden bg-gray-100')}>
<Sidebar className="border-r border-highlight" /> <Sidebar className="border-r border-highlight" />
</div> </div>
<ResizeHandle <ResizeHandle
@@ -173,7 +173,7 @@ function HeaderSize({ className, ...props }: HeaderSizeProps) {
const platform = useOsInfo(); const platform = useOsInfo();
return ( return (
<div <div
className={classnames( className={classNames(
className, className,
'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b', 'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b',
platform?.osType === 'Darwin' && 'pl-20', platform?.osType === 'Darwin' && 'pl-20',

View File

@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import classnames from 'classnames'; import classNames from 'classnames';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
@@ -15,6 +15,7 @@ import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
type Props = { type Props = {
className?: string; className?: string;
@@ -24,6 +25,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null; const activeWorkspaceId = activeWorkspace?.id ?? null;
const environmentId = useActiveEnvironmentId();
const createWorkspace = useCreateWorkspace({ navigateAfter: true }); const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId); const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace); const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
@@ -53,7 +55,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
color="gray" color="gray"
onClick={() => { onClick={() => {
hide(); hide();
routes.navigate('workspace', { workspaceId: w.id }); routes.navigate('workspace', { workspaceId: w.id, environmentId });
}} }}
> >
This Window This Window
@@ -66,7 +68,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
onClick={async () => { onClick={async () => {
hide(); hide();
await invoke('new_window', { await invoke('new_window', {
url: routes.paths.workspace({ workspaceId: w.id }), url: routes.paths.workspace({ workspaceId: w.id, environmentId }),
}); });
}} }}
> >
@@ -150,7 +152,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
<Dropdown items={items}> <Dropdown items={items}>
<Button <Button
size="sm" size="sm"
className={classnames(className, 'text-gray-800 !px-2 truncate')} className={classNames(className, 'text-gray-800 !px-2 truncate')}
forDropdown forDropdown
> >
{activeWorkspace?.name} {activeWorkspace?.name}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import { memo } from 'react'; import { memo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -20,7 +20,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
<HStack <HStack
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
className={classnames(className, 'w-full h-full')} className={classNames(className, 'w-full h-full')}
> >
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center"> <HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
<SidebarActions /> <SidebarActions />

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
interface Props { interface Props {
@@ -9,7 +9,7 @@ export function Banner({ children, className }: Props) {
return ( return (
<div> <div>
<div <div
className={classnames( className={classNames(
className, className,
'border border-red-500 bg-red-300/10 text-red-800 px-3 py-2 rounded select-auto cursor-text', 'border border-red-500 bg-red-300/10 text-red-800 px-3 py-2 rounded select-auto cursor-text',
)} )}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, memo, useMemo } from 'react'; import { forwardRef, memo, useMemo } from 'react';
import { Icon } from './Icon'; import { Icon } from './Icon';
@@ -45,7 +45,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
) { ) {
const classes = useMemo( const classes = useMemo(
() => () =>
classnames( classNames(
className, className,
'flex-shrink-0 outline-none whitespace-nowrap', 'flex-shrink-0 outline-none whitespace-nowrap',
'focus-visible-or-class:ring', 'focus-visible-or-class:ring',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Icon } from './Icon'; import { Icon } from './Icon';
@@ -20,7 +20,7 @@ export function Checkbox({ checked, onChange, className, disabled }: Props) {
aria-checked={checked ? 'true' : 'false'} aria-checked={checked ? 'true' : 'false'}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
className={classnames( className={classNames(
className, className,
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded', 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
'focus:border-focus', 'focus:border-focus',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
count: number; count: number;
@@ -10,7 +10,7 @@ export function CountBadge({ count, className }: Props) {
return ( return (
<div <div
aria-hidden aria-hidden
className={classnames( className={classNames(
className, className,
'opacity-70 border border-highlight text-3xs rounded mb-0.5 px-1 ml-1 h-4 font-mono', 'opacity-70 border border-highlight text-3xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
)} )}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -51,7 +51,7 @@ export function Dialog({
<motion.div <motion.div
initial={{ top: 5, scale: 0.97 }} initial={{ top: 5, scale: 0.97 }}
animate={{ top: 0, scale: 1 }} animate={{ top: 0, scale: 1 }}
className={classnames( className={classNames(
className, className,
'gap-2 grid grid-rows-[auto_minmax(0,1fr)]', 'gap-2 grid grid-rows-[auto_minmax(0,1fr)]',
'relative bg-gray-50 pointer-events-auto', 'relative bg-gray-50 pointer-events-auto',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
@@ -278,7 +278,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
dir="ltr" dir="ltr"
ref={containerRef} ref={containerRef}
style={containerStyles} style={containerStyles}
className={classnames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')} className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
> >
<span <span
aria-hidden aria-hidden
@@ -290,7 +290,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
space={0.5} space={0.5}
ref={initMenu} ref={initMenu}
style={menuStyles} style={menuStyles}
className={classnames( className={classNames(
className, className,
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border', 'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
'border-gray-200 overflow-auto mb-1 mx-0.5', 'border-gray-200 overflow-auto mb-1 mx-0.5',
@@ -356,7 +356,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onFocus={handleFocus} onFocus={handleFocus}
onClick={handleClick} onClick={handleClick}
justify="start" justify="start"
className={classnames( className={classNames(
className, className,
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap', 'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded', 'focus:bg-highlight focus:text-gray-900 rounded',
@@ -366,7 +366,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
> >
{item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>} {item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
<div <div
className={classnames( className={classNames(
// Add padding on right when no right slot, for some visual balance // Add padding on right when no right slot, for some visual balance
!item.rightSlot && 'pr-4', !item.rightSlot && 'pr-4',
)} )}

View File

@@ -2,7 +2,7 @@ import { defaultKeymap } from '@codemirror/commands';
import { Compartment, EditorState, Transaction } from '@codemirror/state'; import { Compartment, EditorState, Transaction } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view'; import type { ViewUpdate } from '@codemirror/view';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import classnames from 'classnames'; import classNames from 'classnames';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import type { MutableRefObject, ReactNode } from 'react'; import type { MutableRefObject, ReactNode } from 'react';
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
@@ -168,7 +168,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
const cmContainer = ( const cmContainer = (
<div <div
ref={initEditorRef} ref={initEditorRef}
className={classnames( className={classNames(
className, className,
'cm-wrapper text-base bg-gray-50', 'cm-wrapper text-base bg-gray-50',
type === 'password' && 'cm-obscure-text', type === 'password' && 'cm-obscure-text',

View File

@@ -1,10 +1,17 @@
import classNames from 'classnames';
interface Props { interface Props {
children: string; children: string;
} }
export function FormattedError({ children }: Props) { export function FormattedError({ children }: Props) {
return ( return (
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal border border-red-500 border-dashed"> <pre
className={classNames(
'text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
'whitespace-normal border border-red-500 border-dashed',
)}
>
{children} {children}
</pre> </pre>
); );

View File

@@ -1,9 +1,9 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
export function Heading({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) { export function Heading({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
return ( return (
<h1 className={classnames(className, 'text-2xl font-semibold text-gray-900 mb-3')} {...props}> <h1 className={classNames(className, 'text-2xl font-semibold text-gray-900 mb-3')} {...props}>
{children} {children}
</h1> </h1>
); );

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
modifier: 'Meta' | 'Control' | 'Shift'; modifier: 'Meta' | 'Control' | 'Shift';
@@ -13,7 +13,7 @@ const keys: Record<Props['modifier'], string> = {
export function HotKey({ modifier, keyName }: Props) { export function HotKey({ modifier, keyName }: Props) {
return ( return (
<span className={classnames('text-sm text-gray-600')}> <span className={classNames('text-sm text-gray-600')}>
{keys[modifier]} {keys[modifier]}
{keyName} {keyName}
</span> </span>

View File

@@ -36,7 +36,7 @@ import {
TriangleRightIcon, TriangleRightIcon,
UpdateIcon, UpdateIcon,
} from '@radix-ui/react-icons'; } from '@radix-ui/react-icons';
import classnames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { memo } from 'react'; import { memo } from 'react';
import { ReactComponent as LeftPanelHiddenIcon } from '../../assets/icons/LeftPanelHiddenIcon.svg'; import { ReactComponent as LeftPanelHiddenIcon } from '../../assets/icons/LeftPanelHiddenIcon.svg';
@@ -95,7 +95,7 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className }: I
const Component = icons[icon] ?? icons.question; const Component = icons[icon] ?? icons.question;
return ( return (
<Component <Component
className={classnames( className={classNames(
className, className,
'text-inherit', 'text-inherit',
size === 'md' && 'h-4 w-4', size === 'md' && 'h-4 w-4',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { forwardRef, useCallback } from 'react'; import { forwardRef, useCallback } from 'react';
import { useTimedBoolean } from '../../hooks/useTimedBoolean'; import { useTimedBoolean } from '../../hooks/useTimedBoolean';
@@ -45,7 +45,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
disabled={icon === 'empty'} disabled={icon === 'empty'}
tabIndex={tabIndex ?? icon === 'empty' ? -1 : undefined} tabIndex={tabIndex ?? icon === 'empty' ? -1 : undefined}
onClick={handleClick} onClick={handleClick}
className={classnames( className={classNames(
className, className,
'flex-shrink-0 text-gray-700 hover:text-gray-1000', 'flex-shrink-0 text-gray-700 hover:text-gray-1000',
'!px-0', '!px-0',
@@ -60,7 +60,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
size={iconSize} size={iconSize}
icon={confirmed ? 'check' : icon} icon={confirmed ? 'check' : icon}
spin={spin} spin={spin}
className={classnames( className={classNames(
iconClassName, iconClassName,
props.disabled && 'opacity-70', props.disabled && 'opacity-70',
confirmed && 'text-green-600', confirmed && 'text-green-600',

View File

@@ -1,10 +1,10 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) { export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
return ( return (
<code <code
className={classnames( className={classNames(
className, className,
'font-mono text-sm bg-highlight border-0 border-gray-200 px-1.5 py-0.5 rounded text-gray-800 shadow-inner', 'font-mono text-sm bg-highlight border-0 border-gray-200 px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
)} )}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useState } from 'react'; import { forwardRef, useCallback, useMemo, useState } from 'react';
@@ -68,7 +68,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
}, [onBlur]); }, [onBlur]);
const id = `input-${name}`; const id = `input-${name}`;
const inputClassName = classnames( const inputClassName = classNames(
className, className,
'!bg-transparent min-w-0 h-full w-full focus:outline-none placeholder:text-placeholder', '!bg-transparent min-w-0 h-full w-full focus:outline-none placeholder:text-placeholder',
// Bump things over if the slots are occupied // Bump things over if the slots are occupied
@@ -94,7 +94,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
<VStack className="w-full"> <VStack className="w-full">
<label <label
htmlFor={id} htmlFor={id}
className={classnames( className={classNames(
labelClassName, labelClassName,
'font-semibold text-xs uppercase text-gray-700', 'font-semibold text-xs uppercase text-gray-700',
hideLabel && 'sr-only', hideLabel && 'sr-only',
@@ -104,7 +104,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
</label> </label>
<HStack <HStack
alignItems="center" alignItems="center"
className={classnames( className={classNames(
containerClassName, containerClassName,
'relative w-full rounded-md text-gray-900', 'relative w-full rounded-md text-gray-900',
'border', 'border',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd'; import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
@@ -134,7 +134,7 @@ export const PairEditor = memo(function PairEditor({
return ( return (
<div <div
className={classnames( className={classNames(
className, className,
'@container', '@container',
'pb-2 grid overflow-auto max-h-full', 'pb-2 grid overflow-auto max-h-full',
@@ -264,7 +264,7 @@ const FormRow = memo(function FormRow({
return ( return (
<div <div
ref={ref} ref={ref}
className={classnames( className={classNames(
className, className,
'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]', 'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
'grid-rows-1 items-center', 'grid-rows-1 items-center',
@@ -273,7 +273,7 @@ const FormRow = memo(function FormRow({
> >
{!isLast ? ( {!isLast ? (
<div <div
className={classnames( className={classNames(
'py-2 h-7 w-3 flex items-center', 'py-2 h-7 w-3 flex items-center',
'justify-center opacity-0 hover:opacity-100', 'justify-center opacity-0 hover:opacity-100',
)} )}
@@ -286,11 +286,11 @@ const FormRow = memo(function FormRow({
<Checkbox <Checkbox
disabled={isLast} disabled={isLast}
checked={isLast ? false : !!pairContainer.pair.enabled} checked={isLast ? false : !!pairContainer.pair.enabled}
className={classnames('mr-2', isLast && '!opacity-disabled')} className={classNames('mr-2', isLast && '!opacity-disabled')}
onChange={handleChangeEnabled} onChange={handleChangeEnabled}
/> />
<div <div
className={classnames( className={classNames(
'grid items-center', 'grid items-center',
'@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]', '@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]',
'gap-0.5 grid-cols-1 grid-rows-2', 'gap-0.5 grid-cols-1 grid-rows-2',
@@ -303,7 +303,7 @@ const FormRow = memo(function FormRow({
validate={nameValidate} validate={nameValidate}
useTemplating useTemplating
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
containerClassName={classnames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
defaultValue={pairContainer.pair.name} defaultValue={pairContainer.pair.name}
label="Name" label="Name"
name="name" name="name"
@@ -315,7 +315,7 @@ const FormRow = memo(function FormRow({
<Input <Input
hideLabel hideLabel
size="sm" size="sm"
containerClassName={classnames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate} validate={valueValidate}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
defaultValue={pairContainer.pair.value} defaultValue={pairContainer.pair.value}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
orientation?: 'horizontal' | 'vertical'; orientation?: 'horizontal' | 'vertical';
@@ -14,10 +14,10 @@ export function Separator({
label, label,
}: Props) { }: Props) {
return ( return (
<div role="separator" className={classnames(className, 'flex items-center')}> <div role="separator" className={classNames(className, 'flex items-center')}>
{label && <div className="text-xs text-gray-500 mx-2 whitespace-nowrap">{label}</div>} {label && <div className="text-xs text-gray-500 mx-2 whitespace-nowrap">{label}</div>}
<div <div
className={classnames( className={classNames(
variant === 'primary' && 'bg-highlight', variant === 'primary' && 'bg-highlight',
variant === 'secondary' && 'bg-highlightSecondary', variant === 'secondary' && 'bg-highlightSecondary',
orientation === 'horizontal' && 'w-full h-[1px]', orientation === 'horizontal' && 'w-full h-[1px]',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from 'react'; import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
@@ -25,7 +25,7 @@ export const HStack = forwardRef(function HStack(
return ( return (
<BaseStack <BaseStack
ref={ref} ref={ref}
className={classnames(className, 'flex-row', space != null && gapClasses[space])} className={classNames(className, 'flex-row', space != null && gapClasses[space])}
{...props} {...props}
> >
{children} {children}
@@ -45,7 +45,7 @@ export const VStack = forwardRef(function VStack(
return ( return (
<BaseStack <BaseStack
ref={ref} ref={ref}
className={classnames(className, 'flex-col', space != null && gapClasses[space])} className={classNames(className, 'flex-col', space != null && gapClasses[space])}
{...props} {...props}
> >
{children} {children}
@@ -69,7 +69,7 @@ const BaseStack = forwardRef(function BaseStack(
return ( return (
<Component <Component
ref={ref} ref={ref}
className={classnames( className={classNames(
className, className,
'flex', 'flex',
alignItems === 'center' && 'items-center', alignItems === 'center' && 'items-center',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { HttpResponse } from '../../lib/models'; import type { HttpResponse } from '../../lib/models';
interface Props { interface Props {
@@ -12,7 +12,7 @@ export function StatusTag({ response, className, showReason }: Props) {
const label = error ? 'ERR' : status; const label = error ? 'ERR' : status;
return ( return (
<span <span
className={classnames( className={classNames(
className, className,
'font-mono', 'font-mono',
status >= 0 && status < 100 && 'text-red-600', status >= 0 && status < 100 && 'text-red-600',

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react'; import { memo, useCallback, useEffect, useRef } from 'react';
import { Button } from '../Button'; import { Button } from '../Button';
@@ -67,11 +67,11 @@ export function Tabs({
return ( return (
<div <div
ref={ref} ref={ref}
className={classnames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')} className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
> >
<div <div
aria-label={label} aria-label={label}
className={classnames( className={classNames(
tabListClassName, tabListClassName,
'flex items-center overflow-x-auto overflow-y-visible hide-scrollbars mt-1 mb-2', 'flex items-center overflow-x-auto overflow-y-visible hide-scrollbars mt-1 mb-2',
// Give space for button focus states within overflow boundary. // Give space for button focus states within overflow boundary.
@@ -81,7 +81,7 @@ export function Tabs({
<HStack space={2} className="flex-shrink-0"> <HStack space={2} className="flex-shrink-0">
{tabs.map((t) => { {tabs.map((t) => {
const isActive = t.value === value; const isActive = t.value === value;
const btnClassName = classnames( const btnClassName = classNames(
isActive ? '' : 'text-gray-600 hover:text-gray-800', isActive ? '' : 'text-gray-600 hover:text-gray-800',
'!px-2 ml-[1px]', '!px-2 ml-[1px]',
); );
@@ -108,7 +108,7 @@ export function Tabs({
: option?.label ?? 'Unknown'} : option?.label ?? 'Unknown'}
<Icon <Icon
icon="triangleDown" icon="triangleDown"
className={classnames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')} className={classNames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')}
/> />
</Button> </Button>
</RadioDropdown> </RadioDropdown>
@@ -149,7 +149,7 @@ export const TabContent = memo(function TabContent({
<div <div
tabIndex={-1} tabIndex={-1}
data-tab={value} data-tab={value}
className={classnames(className, 'tab-content', 'hidden w-full h-full')} className={classNames(className, 'tab-content', 'hidden w-full h-full')}
> >
{children} {children}
</div> </div>

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
interface Props { interface Props {
@@ -10,7 +10,7 @@ export function WindowDragRegion({ className, ...props }: Props) {
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className={classnames(className, 'w-full flex-shrink-0')} className={classNames(className, 'w-full flex-shrink-0')}
{...props} {...props}
/> />
); );

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames'; import classNames from 'classnames';
import Papa from 'papaparse'; import Papa from 'papaparse';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useResponseBodyText } from '../../hooks/useResponseBodyText'; import { useResponseBodyText } from '../../hooks/useResponseBodyText';
@@ -21,10 +21,10 @@ export function CsvViewer({ response, className }: Props) {
return ( return (
<div className="overflow-auto h-full"> <div className="overflow-auto h-full">
<table className={classnames(className, 'text-sm')}> <table className={classNames(className, 'text-sm')}>
<tbody> <tbody>
{parsed.data.map((row, i) => ( {parsed.data.map((row, i) => (
<tr key={i} className={classnames('border-l border-t', i > 0 && 'border-b')}> <tr key={i} className={classNames('border-l border-t', i > 0 && 'border-b')}>
{row.map((col, j) => ( {row.map((col, j) => (
<td key={j} className="border-r px-1.5"> <td key={j} className="border-r px-1.5">
{col} {col}

View File

@@ -1,5 +1,5 @@
import { convertFileSrc } from '@tauri-apps/api/tauri'; import { convertFileSrc } from '@tauri-apps/api/tauri';
import classnames from 'classnames'; import classNames from 'classnames';
import type { HttpResponse } from '../../lib/models'; import type { HttpResponse } from '../../lib/models';
interface Props { interface Props {
@@ -17,7 +17,7 @@ export function ImageViewer({ response, className }: Props) {
<img <img
src={src} src={src}
alt="Response preview" alt="Response preview"
className={classnames(className, 'max-w-full max-h-full')} className={classNames(className, 'max-w-full max-h-full')}
/> />
); );
} }

View File

@@ -3,17 +3,13 @@ import type { Environment } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useEnvironments } from './useEnvironments'; import { useEnvironments } from './useEnvironments';
export function useActiveEnvironment(): [Environment | null, (environment: Environment) => void] { export function useActiveEnvironment(): Environment | null {
const [id, setId] = useActiveEnvironmentId(); const id = useActiveEnvironmentId();
const environments = useEnvironments(); const environments = useEnvironments();
const environment = useMemo( const environment = useMemo(
() => environments.find((w) => w.id === id) ?? null, () => environments.find((w) => w.id === id) ?? null,
[environments, id], [environments, id],
); );
const setActiveEnvironment = useCallback((e: Environment) => { return environment;
setId(e.id)
}, [setId]);
return [environment, setActiveEnvironment];
} }

View File

@@ -1,14 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import type { RouteParamsRequest } from './useAppRoutes';
export function useActiveEnvironmentId(): [string | null, (id: string) => void] { export function useActiveEnvironmentId(): string | null {
const [searchParams, setSearchParams] = useSearchParams(); const { environmentId } = useParams<RouteParamsRequest>();
const id = searchParams.get('environmentId') ?? null; if (environmentId == null || environmentId === '__default__') {
return null;
}
const setId = useCallback((id: string) => { return environmentId;
searchParams.set('environmentId', id)
setSearchParams(searchParams);
}, [searchParams, setSearchParams])
return [id, setId];
} }

View File

@@ -1,8 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveRequestId } from './useActiveRequestId';
import type { Environment } from '../lib/models';
export type RouteParamsWorkspace = { export type RouteParamsWorkspace = {
workspaceId: string; workspaceId: string;
environmentId: string | null;
}; };
export type RouteParamsRequest = RouteParamsWorkspace & { export type RouteParamsRequest = RouteParamsWorkspace & {
@@ -13,23 +17,48 @@ export const routePaths = {
workspaces() { workspaces() {
return '/workspaces'; return '/workspaces';
}, },
workspace({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) { workspace(
return `/workspaces/${workspaceId}`; { workspaceId, environmentId } = {
workspaceId: ':workspaceId',
environmentId: ':environmentId',
} as RouteParamsWorkspace,
) {
return `/workspaces/${workspaceId}/environments/${environmentId ?? '__default__'}`;
}, },
request( request(
{ workspaceId, requestId } = { { workspaceId, environmentId, requestId } = {
workspaceId: ':workspaceId', workspaceId: ':workspaceId',
environmentId: ':environmentId',
requestId: ':requestId', requestId: ':requestId',
} as RouteParamsRequest, } as RouteParamsRequest,
) { ) {
return `${this.workspace({ workspaceId })}/requests/${requestId}`; return `${this.workspace({ workspaceId, environmentId })}/requests/${requestId}`;
}, },
}; };
export function useAppRoutes() { export function useAppRoutes() {
const workspaceId = useActiveWorkspaceId();
const requestId = useActiveRequestId();
const navigate = useNavigate(); const navigate = useNavigate();
return useMemo( return useMemo(
() => ({ () => ({
setEnvironment({ id: environmentId }: Environment) {
if (workspaceId == null) {
this.navigate('workspaces');
} else if (requestId == null) {
this.navigate('workspace', {
workspaceId: workspaceId,
environmentId: environmentId ?? null,
});
} else {
this.navigate('request', {
workspaceId,
environmentId: environmentId ?? null,
requestId: requestId,
});
}
},
navigate<T extends keyof typeof routePaths>( navigate<T extends keyof typeof routePaths>(
path: T, path: T,
...params: Parameters<(typeof routePaths)[T]> ...params: Parameters<(typeof routePaths)[T]>
@@ -42,6 +71,6 @@ export function useAppRoutes() {
}, },
paths: routePaths, paths: routePaths,
}), }),
[navigate], [navigate, requestId, workspaceId],
); );
} }

View File

@@ -4,18 +4,21 @@ import type { Environment } from '../lib/models';
import { environmentsQueryKey } from './useEnvironments'; import { environmentsQueryKey } from './useEnvironments';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useAppRoutes } from './useAppRoutes';
export function useCreateEnvironment() { export function useCreateEnvironment() {
const environmentId = useActiveEnvironmentId();
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [, setActiveEnvironmentId ] = useActiveEnvironmentId(); const routes = useAppRoutes();
return useMutation<Environment, unknown, Pick<Environment, 'name'>>({ return useMutation<Environment, unknown, Pick<Environment, 'name'>>({
mutationFn: (patch) => { mutationFn: (patch) => {
return invoke('create_environment', { ...patch, workspaceId }); return invoke('create_environment', { ...patch, workspaceId });
}, },
onSuccess: async (environment) => { onSuccess: async (environment) => {
if (workspaceId == null) return; if (workspaceId == null) return;
setActiveEnvironmentId(environment.id); routes.navigate('workspace', { workspaceId, environmentId });
queryClient.setQueryData<Environment[]>( queryClient.setQueryData<Environment[]>(
environmentsQueryKey({ workspaceId }), environmentsQueryKey({ workspaceId }),
(environments) => [...(environments ?? []), environment], (environments) => [...(environments ?? []), environment],