mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 20:00:29 +01:00
Setting to colorize HTTP methods
https://feedback.yaak.app/p/support-colors-for-http-method-in-sidebar
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE settings
|
||||
ADD COLUMN colored_methods BOOLEAN DEFAULT FALSE;
|
||||
@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", disabled: boolean, http: string,
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, coloredMethods: boolean, };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models";
|
||||
import type { Folder } from "./gen_models";
|
||||
import type { GrpcRequest } from "./gen_models";
|
||||
import type { HttpRequest } from "./gen_models";
|
||||
import type { WebsocketRequest } from "./gen_models";
|
||||
import type { Workspace } from "./gen_models";
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
|
||||
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
||||
|
||||
@@ -114,6 +114,7 @@ pub struct Settings {
|
||||
pub theme_light: String,
|
||||
pub update_channel: String,
|
||||
pub editor_keymap: EditorKeymap,
|
||||
pub colored_methods: bool,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Settings {
|
||||
@@ -160,6 +161,7 @@ impl UpsertModelInfo for Settings {
|
||||
(ThemeDark, self.theme_dark.as_str().into()),
|
||||
(ThemeLight, self.theme_light.as_str().into()),
|
||||
(UpdateChannel, self.update_channel.into()),
|
||||
(ColoredMethods, self.colored_methods.into()),
|
||||
(Proxy, proxy.into()),
|
||||
])
|
||||
}
|
||||
@@ -179,6 +181,7 @@ impl UpsertModelInfo for Settings {
|
||||
SettingsIden::ThemeDark,
|
||||
SettingsIden::ThemeLight,
|
||||
SettingsIden::UpdateChannel,
|
||||
SettingsIden::ColoredMethods,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -205,6 +208,7 @@ impl UpsertModelInfo for Settings {
|
||||
theme_light: row.get("theme_light")?,
|
||||
hide_window_controls: row.get("hide_window_controls")?,
|
||||
update_channel: row.get("update_channel")?,
|
||||
colored_methods: row.get("colored_methods")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ impl<'a> DbContext<'a> {
|
||||
theme_dark: "yaak-dark".to_string(),
|
||||
theme_light: "yaak-light".to_string(),
|
||||
update_channel: "stable".to_string(),
|
||||
colored_methods: false,
|
||||
};
|
||||
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
const r = [...workspaces].sort((a, b) => {
|
||||
return [...workspaces].sort((a, b) => {
|
||||
const aRecentIndex = recentWorkspaces?.indexOf(a.id);
|
||||
const bRecentIndex = recentWorkspaces?.indexOf(b.id);
|
||||
|
||||
@@ -250,7 +250,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
return a.createdAt.localeCompare(b.createdAt);
|
||||
}
|
||||
});
|
||||
return r;
|
||||
}, [recentWorkspaces, workspaces]);
|
||||
|
||||
const groups = useMemo<CommandPaletteGroup[]>(() => {
|
||||
@@ -272,7 +271,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
searchText: resolvedModelNameWithFolders(r),
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
<HttpMethodTag className="text-text-subtlest" request={r} />
|
||||
<HttpMethodTag short className="text-xs" request={r} />
|
||||
<div className="truncate">{resolvedModelNameWithFolders(r)}</div>
|
||||
</HStack>
|
||||
),
|
||||
|
||||
@@ -41,8 +41,7 @@ import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import type { Pair } from './core/PairEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { TabContent, TabItem, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
@@ -50,6 +49,7 @@ import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
|
||||
@@ -138,10 +138,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ||
|
||||
activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART
|
||||
) {
|
||||
const n = Array.isArray(activeRequest.body?.form)
|
||||
numParams = Array.isArray(activeRequest.body?.form)
|
||||
? activeRequest.body.form.filter((p) => p.name).length
|
||||
: 0;
|
||||
numParams = n;
|
||||
}
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
@@ -314,11 +313,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
[activeRequest.id, sendRequest],
|
||||
);
|
||||
|
||||
const handleMethodChange = useCallback(
|
||||
(method: string) => patchModel(activeRequest, { method }),
|
||||
[activeRequest],
|
||||
);
|
||||
|
||||
const handleUrlChange = useCallback(
|
||||
(url: string) => patchModel(activeRequest, { url }),
|
||||
[activeRequest],
|
||||
@@ -335,14 +329,17 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
stateKey={`url.${activeRequest.id}`}
|
||||
key={forceUpdateKey + urlKey}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
placeholder="https://example.com"
|
||||
onPasteOverwrite={handlePaste}
|
||||
autocomplete={autocomplete}
|
||||
onSend={handleSend}
|
||||
onCancel={cancelResponse}
|
||||
onMethodChange={handleMethodChange}
|
||||
onUrlChange={handleUrlChange}
|
||||
leftSlot={
|
||||
<div className="py-0.5">
|
||||
<RequestMethodDropdown request={activeRequest} className="ml-0.5 !h-full" />
|
||||
</div>
|
||||
}
|
||||
forceUpdateKey={updateKey}
|
||||
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
|
||||
...responses.map((r: HttpResponse) => ({
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
<HttpStatusTag className="text-sm" response={r} />
|
||||
<HttpStatusTag short className="text-xs" response={r} />
|
||||
<span className="text-text-subtle">→</span>{' '}
|
||||
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
|
||||
</HStack>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function RecentRequestsDropdown({ className }: Props) {
|
||||
|
||||
recentRequestItems.push({
|
||||
label: resolvedModelName(request),
|
||||
leftSlot: <HttpMethodTag request={request} />,
|
||||
leftSlot: <HttpMethodTag short className="text-xs" request={request} />,
|
||||
onSelect: async () => {
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { HttpRequest, patchModel } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
import { Icon } from './core/Icon';
|
||||
import type { RadioDropdownItem } from './core/RadioDropdown';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
|
||||
type Props = {
|
||||
method: string;
|
||||
request: HttpRequest;
|
||||
className?: string;
|
||||
onChange: (method: string) => void;
|
||||
};
|
||||
|
||||
const radioItems: RadioDropdownItem<string>[] = [
|
||||
@@ -28,10 +29,13 @@ const radioItems: RadioDropdownItem<string>[] = [
|
||||
}));
|
||||
|
||||
export const RequestMethodDropdown = memo(function RequestMethodDropdown({
|
||||
method,
|
||||
onChange,
|
||||
request,
|
||||
className,
|
||||
}: Props) {
|
||||
const handleChange = useCallback(async (method: string) => {
|
||||
await patchModel(request, { method });
|
||||
}, []);
|
||||
|
||||
const itemsAfter = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -49,17 +53,22 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
|
||||
placeholder: 'CUSTOM',
|
||||
});
|
||||
if (newMethod == null) return;
|
||||
onChange(newMethod);
|
||||
await handleChange(newMethod);
|
||||
},
|
||||
},
|
||||
],
|
||||
[onChange],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<RadioDropdown value={method} items={radioItems} itemsAfter={itemsAfter} onChange={onChange}>
|
||||
<RadioDropdown
|
||||
value={request.method}
|
||||
items={radioItems}
|
||||
itemsAfter={itemsAfter}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<Button size="xs" className={classNames(className, 'text-text-subtle hover:text')}>
|
||||
{method.toUpperCase()}
|
||||
<HttpMethodTag request={request}/>
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
|
||||
@@ -122,6 +122,11 @@ export function SettingsAppearance() {
|
||||
title="Wrap Editor Lines"
|
||||
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.coloredMethods}
|
||||
title="Colorize Request Methods"
|
||||
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
||||
/>
|
||||
|
||||
{type() !== 'macos' && (
|
||||
<Checkbox
|
||||
|
||||
@@ -9,11 +9,9 @@ import { IconButton } from './core/IconButton';
|
||||
import type { InputProps } from './core/Input';
|
||||
import { Input } from './core/Input';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
|
||||
type Props = Pick<HttpRequest, 'url'> & {
|
||||
className?: string;
|
||||
method: HttpRequest['method'] | null;
|
||||
placeholder: string;
|
||||
onSend: () => void;
|
||||
onUrlChange: (url: string) => void;
|
||||
@@ -21,10 +19,10 @@ type Props = Pick<HttpRequest, 'url'> & {
|
||||
onPasteOverwrite?: InputProps['onPasteOverwrite'];
|
||||
onCancel: () => void;
|
||||
submitIcon?: IconProps['icon'] | null;
|
||||
onMethodChange?: (method: string) => void;
|
||||
isLoading: boolean;
|
||||
forceUpdateKey: string;
|
||||
rightSlot?: ReactNode;
|
||||
leftSlot?: ReactNode;
|
||||
autocomplete?: InputProps['autocomplete'];
|
||||
stateKey: InputProps['stateKey'];
|
||||
};
|
||||
@@ -33,16 +31,15 @@ export const UrlBar = memo(function UrlBar({
|
||||
forceUpdateKey,
|
||||
onUrlChange,
|
||||
url,
|
||||
method,
|
||||
placeholder,
|
||||
className,
|
||||
onSend,
|
||||
onCancel,
|
||||
onMethodChange,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
submitIcon = 'send_horizontal',
|
||||
autocomplete,
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
isLoading,
|
||||
stateKey,
|
||||
@@ -87,18 +84,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
onChange={onUrlChange}
|
||||
defaultValue={url}
|
||||
placeholder={placeholder}
|
||||
leftSlot={
|
||||
method != null &&
|
||||
onMethodChange != null && (
|
||||
<div className="py-0.5">
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={onMethodChange}
|
||||
className="ml-0.5 !h-full"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
leftSlot={leftSlot}
|
||||
rightSlot={
|
||||
<HStack space={0.5}>
|
||||
{rightSlot && <div className="py-0.5 h-full">{rightSlot}</div>}
|
||||
|
||||
@@ -642,7 +642,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
justify="start"
|
||||
leftSlot={
|
||||
(isLoading || item.leftSlot) && (
|
||||
<div className={classNames('pr-2 flex justify-start opacity-70')}>
|
||||
<div className={classNames('pr-2 flex justify-start [&_svg]:opacity-70')}>
|
||||
{isLoading ? <LoadingIcon /> : item.leftSlot}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { GrpcRequest, HttpRequest, settingsAtom, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
className?: string;
|
||||
shortNames?: boolean;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
const methodNames: Record<string, string> = {
|
||||
@@ -18,7 +19,8 @@ const methodNames: Record<string, string> = {
|
||||
query: 'QURY',
|
||||
};
|
||||
|
||||
export function HttpMethodTag({ request, className }: Props) {
|
||||
export function HttpMethodTag({ request, className, short }: Props) {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const method =
|
||||
request.model === 'http_request' && request.bodyType === 'graphql'
|
||||
? 'GQL'
|
||||
@@ -26,19 +28,34 @@ export function HttpMethodTag({ request, className }: Props) {
|
||||
? 'GRPC'
|
||||
: request.model === 'websocket_request'
|
||||
? 'WS'
|
||||
: (methodNames[request.method.toLowerCase()] ?? request.method.slice(0, 4));
|
||||
: request.method;
|
||||
let label = method.toUpperCase();
|
||||
|
||||
const paddedMethod = method.padStart(4, ' ').toUpperCase();
|
||||
if (short) {
|
||||
label = methodNames[method.toLowerCase()] ?? method.slice(0, 4);
|
||||
label = label.padStart(4, ' ');
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
'text-xs font-mono text-text-subtle flex-shrink-0 whitespace-pre',
|
||||
!settings.coloredMethods && 'text-text-subtle',
|
||||
settings.coloredMethods && method === 'GQL' && 'text-info',
|
||||
settings.coloredMethods && method === 'WS' && 'text-info',
|
||||
settings.coloredMethods && method === 'GRPC' && 'text-info',
|
||||
settings.coloredMethods && method === 'OPTIONS' && 'text-info',
|
||||
settings.coloredMethods && method === 'HEAD' && 'text-info',
|
||||
settings.coloredMethods && method === 'GET' && 'text-primary',
|
||||
settings.coloredMethods && method === 'PUT' && 'text-warning',
|
||||
settings.coloredMethods && method === 'PATCH' && 'text-notice',
|
||||
settings.coloredMethods && method === 'POST' && 'text-success',
|
||||
settings.coloredMethods && method === 'DELETE' && 'text-danger',
|
||||
'font-mono flex-shrink-0 whitespace-pre',
|
||||
'pt-[0.25em]', // Fix for monospace font not vertically centering
|
||||
)}
|
||||
>
|
||||
{paddedMethod}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@ interface Props {
|
||||
response: HttpResponse;
|
||||
className?: string;
|
||||
showReason?: boolean;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
export function HttpStatusTag({ response, className, showReason }: Props) {
|
||||
export function HttpStatusTag({ response, className, showReason, short }: Props) {
|
||||
const { status, state } = response;
|
||||
|
||||
let colorClass;
|
||||
let label = `${status}`;
|
||||
|
||||
if (state === 'initialized') {
|
||||
label = 'CONNECTING';
|
||||
label = short ? 'CONN' : 'CONNECTING';
|
||||
colorClass = 'text-text-subtle';
|
||||
} else if (status < 100) {
|
||||
label = 'ERROR';
|
||||
label = short ? 'ERR' : 'ERROR';
|
||||
colorClass = 'text-danger';
|
||||
} else if (status < 200) {
|
||||
colorClass = 'text-info';
|
||||
@@ -33,8 +34,7 @@ export function HttpStatusTag({ response, className, showReason }: Props) {
|
||||
|
||||
return (
|
||||
<span className={classNames(className, 'font-mono', colorClass)}>
|
||||
{label}{' '}
|
||||
{showReason && 'statusReason' in response ? response.statusReason : null}
|
||||
{label} {showReason && 'statusReason' in response ? response.statusReason : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ export type InputProps = Pick<
|
||||
> & {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
inputWrapperClassName?: string;
|
||||
defaultValue?: string | null;
|
||||
disableObscureToggle?: boolean;
|
||||
fullHeight?: boolean;
|
||||
hideLabel?: boolean;
|
||||
inputWrapperClassName?: string;
|
||||
help?: ReactNode;
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
|
||||
@@ -12,7 +12,7 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { activeRequestAtom } from '../../hooks/useActiveRequest';
|
||||
import {allRequestsAtom} from "../../hooks/useAllRequests";
|
||||
import { allRequestsAtom } from '../../hooks/useAllRequests';
|
||||
import { useScrollIntoView } from '../../hooks/useScrollIntoView';
|
||||
import { useSidebarItemCollapsed } from '../../hooks/useSidebarItemCollapsed';
|
||||
import { jotaiStore } from '../../lib/jotai';
|
||||
@@ -214,10 +214,13 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
return null;
|
||||
}
|
||||
|
||||
const opacitySubtle = 'opacity-80';
|
||||
|
||||
const itemPrefix = item.model !== 'folder' && (
|
||||
<HttpMethodTag
|
||||
short
|
||||
request={item}
|
||||
className={classNames(!(active || selected) && 'text-text-subtlest')}
|
||||
className={classNames('text-xs', !(active || selected) && opacitySubtle)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -287,7 +290,11 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
{latestHttpResponse.state !== 'closed' ? (
|
||||
<LoadingIcon size="sm" className="text-text-subtlest" />
|
||||
) : (
|
||||
<HttpStatusTag className="text-xs" response={latestHttpResponse} />
|
||||
<HttpStatusTag
|
||||
short
|
||||
className={classNames('text-xs', !(active || selected) && opacitySubtle)}
|
||||
response={latestHttpResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user