mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 05:56:47 +01:00
Compare commits
23 Commits
v2023.0.3
...
v2023.0.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10af9b6f99 | ||
|
|
aa8c066f2d | ||
|
|
b913b74449 | ||
|
|
b71adce50b | ||
|
|
0fbb44c701 | ||
|
|
de335e8637 | ||
|
|
2999f63a4c | ||
|
|
2abc5e6f0b | ||
|
|
639de4321e | ||
|
|
b3c461afdd | ||
|
|
7d154800a0 | ||
|
|
b48ed0399e | ||
|
|
c5d6e7d74a | ||
|
|
e82f915363 | ||
|
|
3128e9ce76 | ||
|
|
bc0e86757c | ||
|
|
fec99916c2 | ||
|
|
3b5d059b11 | ||
|
|
c3fe2acc8a | ||
|
|
4d002c412b | ||
|
|
46d152b5f1 | ||
|
|
25fa81ebbc | ||
|
|
7c2de3c360 |
27
.github/workflows/artifacts.yml
vendored
27
.github/workflows/artifacts.yml
vendored
@@ -5,17 +5,22 @@ on:
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# platform: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
platform: [ macos-latest ]
|
||||
include:
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
@@ -29,7 +34,7 @@ jobs:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
@@ -37,7 +42,9 @@ jobs:
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
# Pin dev version to get non-default targets
|
||||
# https://github.com/tauri-apps/tauri-action/issues/356
|
||||
- uses: tauri-apps/tauri-action@dev
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
@@ -49,9 +56,9 @@ jobs:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: 'App v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
tagName: 'v__VERSION__'
|
||||
releaseName: 'Release __VERSION__'
|
||||
releaseBody: '<!-- Release Notes -->'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: '--target universal-apple-darwin'
|
||||
args: '--target ${{ matrix.target }}'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<script src="http://localhost:8097"></script>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"tauri-dev": "YAAK_ENV=development tauri dev",
|
||||
"tauri-build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "npm run build:frontend",
|
||||
"dev": "vite dev",
|
||||
"lint": "tsc && eslint . --ext .ts,.tsx",
|
||||
|
||||
8
src-tauri/macos/entitlements.plist
Normal file
8
src-tauri/macos/entitlements.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -79,11 +79,8 @@ async fn actually_send_ephemeral_request(
|
||||
let start = std::time::Instant::now();
|
||||
let mut url_string = request.url.to_string();
|
||||
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert("PROJECT_ID", "project_123");
|
||||
variables.insert("TOKEN", "s3cret");
|
||||
variables.insert("DOMAIN", "schier.co");
|
||||
variables.insert("BASE_URL", "https://schier.co");
|
||||
let variables: HashMap<&str, &str> = HashMap::new();
|
||||
// variables.insert("", "");
|
||||
|
||||
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex");
|
||||
url_string = re
|
||||
@@ -104,6 +101,7 @@ async fn actually_send_ephemeral_request(
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
@@ -188,12 +186,23 @@ async fn actually_send_ephemeral_request(
|
||||
|
||||
let raw_response = client.execute(sendable_req).await;
|
||||
|
||||
let p = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/plugin.ts")
|
||||
.expect("failed to resolve resource");
|
||||
let plugin_rel_path = "plugins/plugin.ts";
|
||||
let plugin_path = match app_handle.path_resolver().resolve_resource(plugin_rel_path) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return response_err(
|
||||
response,
|
||||
format!("Plugin not found at {}", plugin_rel_path),
|
||||
&app_handle,
|
||||
pool,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
runtime::run_plugin_sync(p.to_str().unwrap()).unwrap();
|
||||
if let Err(e) = runtime::run_plugin_sync(plugin_path.to_str().unwrap()) {
|
||||
return response_err(response, e.to_string(), &app_handle, pool).await;
|
||||
}
|
||||
|
||||
match raw_response {
|
||||
Ok(v) => {
|
||||
@@ -608,6 +617,14 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
.accelerator("CmdOrCtrl+n"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
.accelerator("CmdOrCtrl+d"),
|
||||
)
|
||||
.add_item(CustomMenuItem::new("new_window".to_string(), "New Window"));
|
||||
if is_dev() {
|
||||
test_menu = test_menu
|
||||
@@ -652,6 +669,8 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
|
||||
"toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(),
|
||||
"focus_url" => win2.emit("focus_url", true).unwrap(),
|
||||
"send_request" => win2.emit("send_request", true).unwrap(),
|
||||
"new_request" => _ = win2.emit("new_request", true).unwrap(),
|
||||
"duplicate_request" => _ = win2.emit("duplicate_request", true).unwrap(),
|
||||
"refresh" => win2.eval("location.reload()").unwrap(),
|
||||
"new_window" => _ = create_window(&handle2),
|
||||
"toggle_devtools" => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.0.3"
|
||||
"version": "2023.0.12"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"macOS": {
|
||||
"exceptionDomain": "",
|
||||
"entitlements": "macos/entitlements.plist",
|
||||
"frameworks": []
|
||||
},
|
||||
"windows": {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { Suspense } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -16,26 +13,12 @@ const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
refetchOnWindowFocus: true,
|
||||
networkMode: 'offlineFirst',
|
||||
|
||||
// It's a desktop app, so this isn't necessary
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const localStoragePersister = createSyncStoragePersister({
|
||||
storage: window.localStorage,
|
||||
throttleTime: 1000, // 1 second
|
||||
});
|
||||
|
||||
persistQueryClient({
|
||||
queryClient,
|
||||
persister: localStoragePersister,
|
||||
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -46,7 +29,7 @@ export function App() {
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
<TauriListeners />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
|
||||
</Suspense>
|
||||
</DialogProvider>
|
||||
</DndProvider>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
Outlet,
|
||||
RouterProvider,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
|
||||
import { routePaths } from '../hooks/useRoutes';
|
||||
import { setLastLocation } from '../lib/lastLocation';
|
||||
import RouteError from './RouteError';
|
||||
import Workspace from './Workspace';
|
||||
import Workspaces from './Workspaces';
|
||||
@@ -16,7 +8,6 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
errorElement: <RouteError />,
|
||||
element: <RouterRoot />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
@@ -44,11 +35,3 @@ const router = createBrowserRouter([
|
||||
export function AppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
function RouterRoot() {
|
||||
const { pathname } = useLocation();
|
||||
useEffect(() => {
|
||||
setLastLocation(pathname).catch(console.error);
|
||||
}, [pathname]);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
const dialog = useDialog();
|
||||
|
||||
return (
|
||||
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||
<div className="pb-2 h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||
<Editor
|
||||
contentType="application/graphql"
|
||||
defaultValue={query ?? ''}
|
||||
|
||||
9
src-web/components/ImageView.tsx
Normal file
9
src-web/components/ImageView.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
interface Props {
|
||||
data: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function ImageView({ data }: Props) {
|
||||
// const dataUri = `data:image/png;base64,${window.btoa(data)}`;
|
||||
return <div>Image preview not supported until binary response support is added</div>;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { useConfirm } from '../hooks/useConfirm';
|
||||
import React from 'react';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useRequest } from '../hooks/useRequest';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HotKey } from './core/HotKey';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
@@ -13,10 +13,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
const request = useRequest(requestId ?? null);
|
||||
const deleteRequest = useDeleteRequest(requestId ?? null);
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||
const confirm = useConfirm();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -25,25 +24,19 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
label: 'Duplicate',
|
||||
onSelect: duplicateRequest.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
rightSlot: <HotKey>⌘D</HotKey>,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onSelect: async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Request',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
deleteRequest.mutate();
|
||||
}
|
||||
},
|
||||
onSelect: deleteRequest.mutate,
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
{ type: 'separator', label: 'Yaak Settings' },
|
||||
{
|
||||
label: appearance === 'dark' ? 'Light Theme' : 'Dark Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -30,8 +30,8 @@ export function ResizeHandle({
|
||||
style={style}
|
||||
className={classnames(
|
||||
className,
|
||||
'group z-10 flex cursor-ew-resize',
|
||||
vertical ? 'w-full h-3 cursor-ns-resize' : 'h-full w-3 cursor-ew-resize',
|
||||
'group z-10 flex',
|
||||
vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize',
|
||||
justify === 'center' && 'justify-center',
|
||||
justify === 'end' && 'justify-end',
|
||||
justify === 'start' && 'justify-start',
|
||||
@@ -46,9 +46,9 @@ export function ResizeHandle({
|
||||
{isResizing && (
|
||||
<div
|
||||
className={classnames(
|
||||
'fixed -left-20 -right-20 -top-20 -bottom-20 cursor-ew-resize',
|
||||
vertical && 'cursor-ns-resize',
|
||||
!vertical && 'cursor-ew-resize',
|
||||
'fixed -left-20 -right-20 -top-20 -bottom-20',
|
||||
vertical && 'cursor-row-resize',
|
||||
!vertical && 'cursor-col-resize',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,13 +8,13 @@ interface Props {
|
||||
|
||||
export function ResponseHeaders({ headers }: Props) {
|
||||
return (
|
||||
<dl className="text-xs w-full font-mono">
|
||||
<dl className="text-xs w-full h-full font-mono overflow-auto">
|
||||
{headers.map((h, i) => {
|
||||
return (
|
||||
<HStack
|
||||
space={3}
|
||||
key={i}
|
||||
className={classnames(i > 0 && 'border-t border-highlightSecondary', 'py-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>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useResponses } from '../hooks/useResponses';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
@@ -17,10 +18,12 @@ import { Editor } from './core/Editor';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusColor } from './core/StatusColor';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { Webview } from './core/Webview';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { ImageView } from './ImageView';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
|
||||
interface Props {
|
||||
@@ -52,9 +55,20 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
[activeResponse],
|
||||
);
|
||||
|
||||
const tabs = useMemo(
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{ label: 'Body', value: 'body' },
|
||||
{
|
||||
value: 'body',
|
||||
label: 'Preview',
|
||||
options: {
|
||||
value: viewMode,
|
||||
onChange: toggleViewMode,
|
||||
items: [
|
||||
{ label: 'Pretty', value: 'pretty' },
|
||||
{ label: 'Raw', value: 'raw' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
@@ -67,9 +81,12 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
value: 'headers',
|
||||
},
|
||||
],
|
||||
[activeResponse?.headers],
|
||||
[activeResponse?.headers, toggleViewMode, viewMode],
|
||||
);
|
||||
|
||||
// Don't render until we know the view mode
|
||||
if (viewMode === undefined) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -80,105 +97,104 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
className={classnames(
|
||||
'italic text-gray-700 text-sm w-full flex-shrink-0',
|
||||
// Remove a bit of space because the tabs have lots too
|
||||
'-mb-1.5',
|
||||
)}
|
||||
>
|
||||
{activeResponse && (
|
||||
<>
|
||||
<div className="whitespace-nowrap p-3 py-2">
|
||||
<StatusColor statusCode={activeResponse.status}>
|
||||
{activeResponse.status}
|
||||
{activeResponse.statusReason && ` ${activeResponse.statusReason}`}
|
||||
</StatusColor>
|
||||
•
|
||||
{activeResponse.elapsed}ms •
|
||||
{Math.round(activeResponse.body.length / 1000)} KB
|
||||
</div>
|
||||
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
||||
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
|
||||
<>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
className={classnames(
|
||||
'italic text-gray-700 text-sm w-full flex-shrink-0',
|
||||
// Remove a bit of space because the tabs have lots too
|
||||
'-mb-1.5',
|
||||
)}
|
||||
>
|
||||
{activeResponse && (
|
||||
<HStack alignItems="center" className="w-full">
|
||||
<div className="whitespace-nowrap px-3">
|
||||
<StatusTag response={activeResponse} />
|
||||
{activeResponse.elapsed > 0 && <> • {activeResponse.elapsed}ms</>}
|
||||
{activeResponse.body.length > 0 && (
|
||||
<> • {(activeResponse.body.length / 1000).toFixed(1)} KB</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified',
|
||||
onSelect: toggleViewMode,
|
||||
},
|
||||
{ type: 'separator', label: 'Actions' },
|
||||
{
|
||||
label: 'Clear Response',
|
||||
onSelect: deleteResponse.mutate,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{
|
||||
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
hidden: responses.length <= 1,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...responses.slice(0, 10).map((r) => ({
|
||||
label: r.status + ' - ' + r.elapsed + ' ms',
|
||||
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
|
||||
onSelect: () => setPinnedResponseId(r.id),
|
||||
})),
|
||||
]}
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: 'Clear Response',
|
||||
onSelect: deleteResponse.mutate,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{
|
||||
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
hidden: responses.length <= 1,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...responses.slice(0, 10).map((r) => ({
|
||||
label: r.status + ' - ' + r.elapsed + ' ms',
|
||||
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
|
||||
onSelect: () => setPinnedResponseId(r.id),
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Show response history"
|
||||
icon="triangleDown"
|
||||
className="ml-auto"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
/>
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Response"
|
||||
tabs={tabs}
|
||||
className="ml-3 mr-1"
|
||||
tabListClassName="mt-1.5"
|
||||
>
|
||||
<IconButton
|
||||
title="Show response history"
|
||||
icon="triangleDown"
|
||||
className="ml-auto"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
/>
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{activeResponse?.error ? (
|
||||
<Banner className="m-2">{activeResponse.error}</Banner>
|
||||
) : (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Response"
|
||||
className="px-3"
|
||||
tabs={tabs}
|
||||
>
|
||||
<TabContent value="body">
|
||||
{activeResponse === null ? (
|
||||
<EmptyStateText>No Response</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
||||
<Webview
|
||||
body={activeResponse.body}
|
||||
contentType={contentType}
|
||||
url={activeResponse.url}
|
||||
/>
|
||||
) : viewMode === 'pretty' && contentType.includes('json') ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={tryFormatJson(activeResponse?.body)}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : activeResponse?.body ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={activeResponse.updatedAt}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={activeResponse?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : null}
|
||||
</TabContent>
|
||||
<TabContent value="headers">
|
||||
<ResponseHeaders headers={activeResponse?.headers ?? []} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
<TabContent value="headers">
|
||||
<ResponseHeaders headers={activeResponse?.headers ?? []} />
|
||||
</TabContent>
|
||||
<TabContent value="body">
|
||||
{!activeResponse.body ? (
|
||||
<EmptyStateText>No Response</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
||||
<Webview
|
||||
body={activeResponse.body}
|
||||
contentType={contentType}
|
||||
url={activeResponse.url}
|
||||
/>
|
||||
) : viewMode === 'pretty' && contentType.includes('json') ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={tryFormatJson(activeResponse?.body)}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : contentType.startsWith('image') ? (
|
||||
<ImageView data={activeResponse?.body} />
|
||||
) : activeResponse?.body ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={activeResponse.updatedAt}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={activeResponse?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : null}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,9 +15,14 @@ export default function RouteError() {
|
||||
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal">
|
||||
{message}
|
||||
</pre>
|
||||
<Button to="/" color="primary">
|
||||
Go Home
|
||||
</Button>
|
||||
<VStack space={2}>
|
||||
<Button to="/" color="primary">
|
||||
Go Home
|
||||
</Button>
|
||||
<Button color="secondary" onClick={() => window.location.reload()}>
|
||||
Refresh
|
||||
</Button>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,16 +4,18 @@ import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useSta
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { Icon } from './core/Icon';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { DropMarker } from './DropMarker';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { ToggleThemeButton } from './ToggleThemeButton';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -45,9 +47,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
>
|
||||
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
|
||||
</VStack>
|
||||
<HStack className="mx-1 pb-1" alignItems="center" justifyContent="end">
|
||||
<ToggleThemeButton />
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -136,7 +135,9 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
{ className, requestName, requestId, workspaceId, active }: SidebarItemProps,
|
||||
ref: ForwardedRef<HTMLLIElement>,
|
||||
) {
|
||||
const latestResponse = useLatestResponse(requestId);
|
||||
const updateRequest = useUpdateRequest(requestId);
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
|
||||
const handleSubmitNameEdit = useCallback(
|
||||
@@ -159,12 +160,17 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
e.preventDefault();
|
||||
setEditing(true);
|
||||
}
|
||||
if (active && (e.key === 'Backspace' || e.key === 'Delete')) {
|
||||
e.preventDefault();
|
||||
deleteRequest.mutate();
|
||||
}
|
||||
},
|
||||
[active],
|
||||
[active, deleteRequest],
|
||||
);
|
||||
|
||||
const handleInputKeyDown = useCallback(
|
||||
async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
@@ -183,21 +189,19 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
|
||||
<div className="relative">
|
||||
<Button
|
||||
tabIndex={0}
|
||||
color="custom"
|
||||
size="sm"
|
||||
size="xs"
|
||||
to={`/workspaces/${workspaceId}/requests/${requestId}`}
|
||||
draggable={false} // Item should drag, not the link
|
||||
onDoubleClick={() => setEditing(true)}
|
||||
onClick={active ? () => setEditing(true) : undefined}
|
||||
justify="start"
|
||||
onKeyDown={handleKeyDown}
|
||||
className={classnames(
|
||||
editing && 'focus-within:border-focus',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
active
|
||||
? 'bg-highlight text-gray-900'
|
||||
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
|
||||
// Move out of the way when trash is shown
|
||||
'group-hover/item:pr-7',
|
||||
)}
|
||||
>
|
||||
{editing ? (
|
||||
@@ -213,19 +217,16 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
{requestName || 'New Request'}
|
||||
</span>
|
||||
)}
|
||||
{latestResponse && (
|
||||
<div className="ml-auto">
|
||||
{isResponseLoading(latestResponse) ? (
|
||||
<Icon spin size="sm" icon="update" />
|
||||
) : (
|
||||
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<RequestActionsDropdown requestId={requestId}>
|
||||
<IconButton
|
||||
color="custom"
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="dotsH"
|
||||
className={classnames(
|
||||
'absolute right-0 top-0 transition-opacity !opacity-0',
|
||||
'group-hover/item:!opacity-100 focus-visible:!opacity-100',
|
||||
)}
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useTauriEvent } from '../hooks/useTauriEvent';
|
||||
import { IconButton } from './core/IconButton';
|
||||
|
||||
export const SidebarActions = memo(function SidebarDisplayToggle() {
|
||||
const { hidden, toggle } = useSidebarHidden();
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const createRequest = useCreateRequest({ navigateAfter: true });
|
||||
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
|
||||
const handleCreateRequest = useCallback(() => {
|
||||
createRequest.mutate({ name: 'New Request' });
|
||||
createRequest.mutate({});
|
||||
}, [createRequest]);
|
||||
useTauriEvent('new_request', () => {
|
||||
createRequest.mutate({});
|
||||
});
|
||||
// TODO: Put this somewhere better
|
||||
useTauriEvent('duplicate_request', () => {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { IconButton } from './core/IconButton';
|
||||
|
||||
export function ToggleThemeButton() {
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
return (
|
||||
<IconButton
|
||||
title={appearance === 'dark' ? 'Enable light mode' : 'Enable dark mode'}
|
||||
icon={appearance === 'dark' ? 'moon' : 'sun'}
|
||||
onClick={toggleAppearance}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import classnames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useConfirm } from '../hooks/useConfirm';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { useRoutes } from '../hooks/useRoutes';
|
||||
@@ -10,7 +9,6 @@ import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -21,9 +19,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const routes = useRoutes();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const items: DropdownItem[] = useMemo(() => {
|
||||
const workspaceItems = workspaces.map((w) => ({
|
||||
@@ -49,20 +46,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
||||
{
|
||||
label: 'Delete Workspace',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Workspace',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{activeWorkspace?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
deleteWorkspace.mutate();
|
||||
}
|
||||
},
|
||||
onSelect: deleteWorkspace.mutate,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="center"
|
||||
|
||||
@@ -46,7 +46,6 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
|
||||
() =>
|
||||
classnames(
|
||||
className,
|
||||
'opacity-90 hover:opacity-100',
|
||||
'outline-none whitespace-nowrap',
|
||||
'focus-visible-or-class:ring',
|
||||
'rounded-md flex items-center',
|
||||
|
||||
@@ -266,7 +266,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
onClick={handleClick}
|
||||
className={classnames(
|
||||
className,
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap pr-4',
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -183,20 +183,16 @@
|
||||
@apply bg-highlight text-gray-900;
|
||||
}
|
||||
|
||||
& > ul > li:hover {
|
||||
@apply text-gray-800;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply text-sm flex items-center pb-0.5;
|
||||
@apply text-sm flex items-center pb-0.5 flex-shrink-0;
|
||||
}
|
||||
|
||||
|
||||
.cm-completionLabel {
|
||||
@apply text-gray-700;
|
||||
}
|
||||
|
||||
.cm-completionDetail {
|
||||
@apply ml-auto;
|
||||
@apply ml-auto pl-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface EditorProps {
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
singleLine?: boolean;
|
||||
wrapLines?: boolean;
|
||||
format?: (v: string) => string;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
actions?: ReactNode;
|
||||
@@ -59,6 +60,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
format,
|
||||
autocomplete,
|
||||
actions,
|
||||
wrapLines,
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
@@ -93,6 +95,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
cm.current?.view.dispatch({ effects: effect });
|
||||
}, [placeholder]);
|
||||
|
||||
// Update wrap lines
|
||||
const wrapLinesCompartment = useRef(new Compartment());
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
const ext = wrapLines ? [EditorView.lineWrapping] : [];
|
||||
const effect = wrapLinesCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects: effect });
|
||||
}, [wrapLines]);
|
||||
|
||||
// Update language extension when contentType changes
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
@@ -126,16 +137,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||
),
|
||||
placeholderCompartment.current.of([]),
|
||||
wrapLinesCompartment.current.of([]),
|
||||
...getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
singleLine,
|
||||
onChange: handleChange,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
readOnly,
|
||||
singleLine,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,18 +3,8 @@ import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
|
||||
const variables = [
|
||||
{ name: 'DOMAIN' },
|
||||
{ name: 'BASE_URL' },
|
||||
{ name: 'CONTENT_THINGY' },
|
||||
{ name: 'TOKEN' },
|
||||
{ name: 'PROJECT_ID' },
|
||||
{ name: 'DUMMY' },
|
||||
{ name: 'DUMMY_2' },
|
||||
{ name: 'STRIPE_PUB_KEY' },
|
||||
{ name: 'RAILWAY_TOKEN' },
|
||||
{ name: 'SECRET' },
|
||||
{ name: 'PORT' },
|
||||
const variables: { name: string }[] = [
|
||||
// TODO: Put variables here
|
||||
];
|
||||
|
||||
const MIN_MATCH_VAR = 2;
|
||||
|
||||
@@ -5,7 +5,7 @@ export function HotKey({ children }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
className={classnames(
|
||||
'bg-gray-400 bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
|
||||
'bg-highlightSecondary bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
|
||||
'font-mono text-gray-500 tracking-widest',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -57,12 +57,12 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
const handleOnFocus = useCallback(() => {
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
const handleOnBlur = useCallback(() => {
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
@@ -107,8 +107,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
className={classnames(
|
||||
containerClassName,
|
||||
'relative w-full rounded-md text-gray-900',
|
||||
'border border-highlight',
|
||||
focused && 'border-focus',
|
||||
'border',
|
||||
focused ? 'border-focus' : 'border-highlight',
|
||||
!isValid && '!border-invalid',
|
||||
size === 'md' && 'h-md leading-md',
|
||||
size === 'sm' && 'h-sm leading-sm',
|
||||
@@ -125,8 +125,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
className={inputClassName}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
{...props}
|
||||
/>
|
||||
{type === 'password' && (
|
||||
|
||||
@@ -137,7 +137,7 @@ export const PairEditor = memo(function PairEditor({
|
||||
className={classnames(
|
||||
className,
|
||||
'@container',
|
||||
'pb-2 grid',
|
||||
'pb-2 grid overflow-auto max-h-full',
|
||||
// Move over the width of the drag handle
|
||||
'-ml-3',
|
||||
)}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
statusCode: number;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function StatusColor({ statusCode, children }: Props) {
|
||||
return (
|
||||
<span
|
||||
className={classnames(
|
||||
statusCode >= 100 && statusCode < 200 && 'text-green-600',
|
||||
statusCode >= 200 && statusCode < 300 && 'text-green-600',
|
||||
statusCode >= 300 && statusCode < 400 && 'text-pink-600',
|
||||
statusCode >= 400 && statusCode < 500 && 'text-orange-600',
|
||||
statusCode >= 500 && statusCode < 600 && 'text-red-600',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
28
src-web/components/core/StatusTag.tsx
Normal file
28
src-web/components/core/StatusTag.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import classnames from 'classnames';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
response: Pick<HttpResponse, 'status' | 'error'>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatusTag({ response, className }: Props) {
|
||||
const { status, error } = response;
|
||||
const label = error ? 'ERR' : status;
|
||||
return (
|
||||
<span
|
||||
className={classnames(
|
||||
className,
|
||||
'font-mono',
|
||||
status >= 0 && status < 100 && 'text-red-600',
|
||||
status >= 100 && status < 200 && 'text-green-600',
|
||||
status >= 200 && status < 300 && 'text-green-600',
|
||||
status >= 300 && status < 400 && 'text-pink-600',
|
||||
status >= 400 && status < 500 && 'text-orange-600',
|
||||
status >= 500 && 'text-red-600',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -81,8 +81,10 @@ export function Tabs({
|
||||
<HStack space={1} className="flex-shrink-0">
|
||||
{tabs.map((t) => {
|
||||
const isActive = t.value === value;
|
||||
// const btnClassName = classnames(isActive ? 'bg-highlightSecondary' : 'text-gray-600');
|
||||
const btnClassName = classnames(isActive ? '' : 'text-gray-600', '!px-0 mr-4 ml-[1px]');
|
||||
const btnClassName = classnames(
|
||||
isActive ? '' : 'text-gray-600 hover:text-gray-800',
|
||||
'!px-0 mr-4 ml-[1px]',
|
||||
);
|
||||
|
||||
if ('options' in t) {
|
||||
const option = t.options.items.find(
|
||||
@@ -147,7 +149,7 @@ export const TabContent = memo(function TabContent({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
data-tab={value}
|
||||
className={classnames(className, 'tab-content', 'overflow-auto hidden w-full h-full')}
|
||||
className={classnames(className, 'tab-content', 'hidden w-full h-full')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -16,12 +16,12 @@ export function Webview({ body, url, contentType }: Props) {
|
||||
}, [url, body, contentType]);
|
||||
|
||||
return (
|
||||
<div className="px-2 pb-2">
|
||||
<div className="h-full pb-3">
|
||||
<iframe
|
||||
title="Response preview"
|
||||
srcDoc={contentForIframe}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="h-full w-full rounded-md border border-gray-100/20"
|
||||
className="h-full w-full rounded border border-highlightSecondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,9 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
}
|
||||
const sortPriority = maxSortPriority(requests) + 1000;
|
||||
return invoke('create_request', { sortPriority, workspaceId, ...patch });
|
||||
patch.name = patch.name || 'New Request';
|
||||
patch.sortPriority = patch.sortPriority || maxSortPriority(requests) + 1000;
|
||||
return invoke('create_request', { workspaceId, ...patch });
|
||||
},
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { useActiveRequestId } from './useActiveRequestId';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
import { useRoutes } from './useRoutes';
|
||||
@@ -10,9 +13,28 @@ export function useDeleteRequest(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const routes = useRoutes();
|
||||
return useMutation<HttpRequest, string>({
|
||||
mutationFn: async () => invoke('delete_request', { requestId: id }),
|
||||
onSuccess: async ({ workspaceId, id: requestId }) => {
|
||||
const confirm = useConfirm();
|
||||
|
||||
return useMutation<HttpRequest | null, string>({
|
||||
mutationFn: async () => {
|
||||
const request = await getRequest(id);
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Request',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
const { workspaceId, id: requestId } = request;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
|
||||
(requests ?? []).filter((r) => r.id !== requestId),
|
||||
@@ -1,20 +1,37 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { useRoutes } from './useRoutes';
|
||||
import { workspacesQueryKey } from './useWorkspaces';
|
||||
|
||||
export function useDeleteWorkspace(id: string | null) {
|
||||
export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const routes = useRoutes();
|
||||
return useMutation<Workspace, string>({
|
||||
mutationFn: () => {
|
||||
return invoke('delete_workspace', { id });
|
||||
const confirm = useConfirm();
|
||||
|
||||
return useMutation<Workspace | null, string>({
|
||||
mutationFn: async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Workspace',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{workspace?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_workspace', { id: workspace?.id });
|
||||
},
|
||||
onSuccess: async ({ id: workspaceId }) => {
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
const { id: workspaceId } = workspace;
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) =>
|
||||
workspaces?.filter((workspace) => workspace.id !== workspaceId),
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useResponses } from './useResponses';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { useLatestResponse } from './useLatestResponse';
|
||||
|
||||
export function useIsResponseLoading(requestId: string | null): boolean {
|
||||
const responses = useResponses(requestId);
|
||||
const response = responses[responses.length - 1];
|
||||
if (!response) return false;
|
||||
return !(response.body || response.status || response.error);
|
||||
const response = useLatestResponse(requestId);
|
||||
if (response === null) return false;
|
||||
return isResponseLoading(response);
|
||||
}
|
||||
|
||||
7
src-web/hooks/useLatestResponse.ts
Normal file
7
src-web/hooks/useLatestResponse.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { useResponses } from './useResponses';
|
||||
|
||||
export function useLatestResponse(requestId: string | null): HttpResponse | null {
|
||||
const responses = useResponses(requestId);
|
||||
return responses[responses.length - 1] ?? null;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { getKeyValue, NAMESPACE_NO_SYNC, setKeyValue } from './keyValueStore';
|
||||
|
||||
export async function getLastLocation(): Promise<string> {
|
||||
return getKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', fallback: '/' });
|
||||
}
|
||||
|
||||
export async function setLastLocation(pathname: string): Promise<void> {
|
||||
return setKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', value: pathname });
|
||||
}
|
||||
|
||||
export async function syncLastLocation(): Promise<void> {
|
||||
const lastPathname = await getLastLocation();
|
||||
if (lastPathname !== window.location.pathname) {
|
||||
console.log(`Redirecting to last location: ${lastPathname}`);
|
||||
window.location.assign(lastPathname);
|
||||
}
|
||||
}
|
||||
@@ -72,3 +72,7 @@ export interface HttpResponse extends BaseModel {
|
||||
readonly url: string;
|
||||
readonly headers: HttpHeader[];
|
||||
}
|
||||
|
||||
export function isResponseLoading(response: HttpResponse): boolean {
|
||||
return !(response.body || response.status || response.error);
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ const darkTheme: AppTheme = {
|
||||
colors: {
|
||||
gray: '#6b5b98',
|
||||
red: '#ff417b',
|
||||
orange: '#ff9411',
|
||||
orange: '#fd9014',
|
||||
yellow: '#e8d13f',
|
||||
green: '#43e76f',
|
||||
green: '#3fd265',
|
||||
blue: '#219dff',
|
||||
pink: '#f670f6',
|
||||
pink: '#ff6dff',
|
||||
violet: '#b176ff',
|
||||
},
|
||||
},
|
||||
@@ -31,11 +31,11 @@ const lightTheme: AppTheme = {
|
||||
colors: {
|
||||
gray: '#7f8fb0',
|
||||
red: '#ec3f87',
|
||||
orange: '#ff8b00',
|
||||
orange: '#ff8000',
|
||||
yellow: '#e7cf24',
|
||||
green: '#00d365',
|
||||
blue: '#0090ff',
|
||||
pink: '#f670f6',
|
||||
pink: '#ea6cea',
|
||||
violet: '#ac6cff',
|
||||
},
|
||||
},
|
||||
@@ -50,14 +50,6 @@ export function getAppearance(): Appearance {
|
||||
return getPreferredAppearance();
|
||||
}
|
||||
|
||||
export function toggleAppearance(): Appearance {
|
||||
const currentTheme =
|
||||
document.documentElement.getAttribute('data-appearance') ?? getPreferredAppearance();
|
||||
const newAppearance = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setAppearance(newAppearance);
|
||||
return newAppearance;
|
||||
}
|
||||
|
||||
export function setAppearance(a?: Appearance) {
|
||||
const appearance = a ?? getPreferredAppearance();
|
||||
const theme = appearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
@@ -2,12 +2,10 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './components/App';
|
||||
import { getKeyValue } from './lib/keyValueStore';
|
||||
import { syncLastLocation } from './lib/lastLocation';
|
||||
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
|
||||
import './main.css';
|
||||
|
||||
setAppearance(await getKeyValue({ key: 'appearance', fallback: getPreferredAppearance() }));
|
||||
await syncLastLocation();
|
||||
|
||||
// root holds our app's root DOM Element:
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
||||
@@ -16,13 +16,13 @@ module.exports = {
|
||||
"xs": "0.8rem"
|
||||
},
|
||||
height: {
|
||||
"xs": "1.5rem",
|
||||
"xs": "1.75rem",
|
||||
"sm": "2.0rem",
|
||||
"md": "2.5rem"
|
||||
},
|
||||
lineHeight: {
|
||||
// HACK: Minus 2 to account for borders inside inputs
|
||||
"xs": "calc(1.5rem - 2px)",
|
||||
"xs": "calc(1.75rem - 2px)",
|
||||
"sm": "calc(2.0rem - 2px)",
|
||||
"md": "calc(2.5rem - 2px)"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user