Flatten migrations, kvs lib, fix tabs

This commit is contained in:
Gregory Schier
2023-03-17 08:36:21 -07:00
parent 637c220475
commit 10616001df
23 changed files with 406 additions and 392 deletions

View File

@@ -5,10 +5,11 @@ import { listen } from '@tauri-apps/api/event';
import { MotionConfig } from 'framer-motion';
import { HelmetProvider } from 'react-helmet-async';
import { matchPath } from 'react-router-dom';
import { keyValueQueryKey } from '../hooks/useKeyValues';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { requestsQueryKey } from '../hooks/useRequests';
import { responsesQueryKey } from '../hooks/useResponses';
import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { extractKeyValue } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, KeyValue } from '../lib/models';
import { convertDates } from '../lib/models';
import { AppRouter, WORKSPACE_REQUEST_PATH } from './AppRouter';
@@ -16,7 +17,7 @@ import { AppRouter, WORKSPACE_REQUEST_PATH } from './AppRouter';
const queryClient = new QueryClient();
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
queryClient.setQueryData(keyValueQueryKey(keyValue), keyValue);
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
});
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {

View File

@@ -1,7 +1,8 @@
import classnames from 'classnames';
import { act } from 'react-dom/test-utils';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useKeyValues } from '../hooks/useKeyValues';
import { useKeyValue } from '../hooks/useKeyValue';
import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters';
@@ -21,7 +22,7 @@ export function RequestPane({ fullHeight, className }: Props) {
const updateRequest = useUpdateRequest(activeRequest);
const sendRequest = useSendRequest(activeRequest);
const responseLoading = useIsResponseLoading();
const activeTab = useKeyValues({
const activeTab = useKeyValue<string>({
key: ['active_request_body_tab', activeRequest?.id ?? 'n/a'],
initialValue: 'body',
});
@@ -61,7 +62,6 @@ export function RequestPane({ fullHeight, className }: Props) {
{ value: 'auth', label: 'Auth' },
]}
className="mt-2"
defaultValue="body"
label="Request body"
>
<TabContent value="headers">

View File

@@ -3,10 +3,11 @@ import React, { useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useKeyValues } from '../hooks/useKeyValues';
import { useKeyValue } from '../hooks/useKeyValue';
import { useRequests } from '../hooks/useRequests';
import { useTheme } from '../hooks/useTheme';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { clamp } from '../lib/clamp';
import type { HttpRequest } from '../lib/models';
import { Button } from './core/Button';
import { Dropdown, DropdownMenuTrigger } from './core/Dropdown';
@@ -19,11 +20,12 @@ interface Props {
className?: string;
}
const MIN_WIDTH = 130;
const MIN_WIDTH = 110;
const MAX_WIDTH = 500;
export function Sidebar({ className }: Props) {
const [isDragging, setIsDragging] = useState<boolean>(false);
const width = useKeyValues<number>({ key: 'sidebar_width', initialValue: 200 });
const width = useKeyValue<number>({ key: 'sidebar_width', initialValue: 200 });
const requests = useRequests();
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest({ navigateAfter: true });
@@ -43,7 +45,7 @@ export function Sidebar({ className }: Props) {
const startWidth = width.value;
moveState.current = {
move: (e: MouseEvent) => {
const newWidth = Math.max(MIN_WIDTH, startWidth + (e.clientX - mouseStartX));
const newWidth = clamp(startWidth + (e.clientX - mouseStartX), MIN_WIDTH, MAX_WIDTH);
width.set(newWidth);
},
up: () => {
@@ -73,8 +75,8 @@ export function Sidebar({ className }: Props) {
>
<div
className={classnames(
'transition-colors w-[1px] group-hover:bg-white/10 h-full pointer-events-none',
isDragging && '!bg-white/20',
'transition-colors w-[1px] group-hover:bg-gray-300 h-full pointer-events-none',
isDragging && '!bg-blue-500/70',
)}
/>
</div>

View File

@@ -10,7 +10,6 @@ import { HStack } from '../Stacks';
import './Tabs.css';
interface Props {
defaultValue?: string;
label: string;
onChangeValue: (value: string) => void;
value: string;
@@ -31,7 +30,6 @@ interface Props {
export function Tabs({
value,
onChangeValue,
defaultValue,
label,
children,
tabs,
@@ -40,7 +38,7 @@ export function Tabs({
}: Props) {
return (
<T.Root
defaultValue={defaultValue}
value={value}
onValueChange={onChangeValue}
className={classnames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
>

View File

@@ -4,5 +4,5 @@ export function useIsResponseLoading(): boolean {
const responses = useResponses();
const response = responses[responses.length - 1];
if (!response) return false;
return !(response.body || response.error);
return !(response.body || response.status || response.error);
}

View File

@@ -0,0 +1,39 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore';
const DEFAULT_NAMESPACE = 'app';
export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE,
key,
}: {
namespace?: string;
key: string | string[];
}) {
return ['key_value', { namespace, key: buildKeyValueKey(key) }];
}
export function useKeyValue<T extends string | number | boolean>({
namespace = DEFAULT_NAMESPACE,
key,
initialValue,
}: {
namespace?: string;
key: string | string[];
initialValue: T;
}) {
const query = useQuery<T>({
initialData: initialValue,
queryKey: keyValueQueryKey({ namespace, key }),
queryFn: async () => getKeyValue({ namespace, key, fallback: initialValue }),
});
const mutate = useMutation<T, unknown, T>({
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
});
return {
value: query.data,
set: (value: T) => mutate.mutate(value),
};
}

View File

@@ -1,57 +0,0 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { KeyValue } from '../lib/models';
const DEFAULT_NAMESPACE = 'app';
export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE,
key,
}: {
namespace?: string;
key: string | string[];
}) {
return ['key_value', { namespace, key: buildKey(key) }];
}
export function useKeyValues<T extends string | number | boolean>({
namespace = DEFAULT_NAMESPACE,
key,
initialValue,
}: {
namespace?: string;
key: string | string[];
initialValue: T;
}) {
const query = useQuery<KeyValue | null>({
initialData: null,
queryKey: keyValueQueryKey({ namespace, key }),
queryFn: async () => invoke('get_key_value', { namespace, key: buildKey(key) }),
});
const mutate = useMutation<KeyValue, unknown, T>({
mutationFn: (value) => {
return invoke('set_key_value', {
namespace,
key: buildKey(key),
value: JSON.stringify(value),
});
},
});
let value: T;
try {
value = JSON.parse(query.data?.value ?? JSON.stringify(initialValue));
} catch (e) {
value = initialValue;
}
return {
value,
set: (value: T) => mutate.mutate(value),
};
}
function buildKey(key: string | string[]): string {
if (typeof key === 'string') return key;
return key.join('::');
}

View File

@@ -1,7 +1,7 @@
import { useKeyValues } from './useKeyValues';
import { useKeyValue } from './useKeyValue';
export function useResponseViewMode(requestId?: string): [string, () => void] {
const v = useKeyValues({
const v = useKeyValue<string>({
namespace: 'app',
key: ['response_view_mode', requestId ?? 'n/a'],
initialValue: 'pretty',

View File

@@ -1,3 +1,4 @@
import { app } from '@tauri-apps/api';
import { useEffect } from 'react';
import type { Appearance } from '../lib/theme/window';
import {
@@ -5,10 +6,13 @@ import {
setAppearance,
subscribeToPreferredAppearanceChange,
} from '../lib/theme/window';
import { useKeyValues } from './useKeyValues';
import { useKeyValue } from './useKeyValue';
export function useTheme() {
const appearanceKv = useKeyValues({ key: 'appearance', initialValue: getAppearance() });
const appearanceKv = useKeyValue<Appearance>({
key: 'appearance',
initialValue: getAppearance(),
});
const themeChange = (appearance: Appearance) => {
appearanceKv.set(appearance);
@@ -22,7 +26,7 @@ export function useTheme() {
useEffect(() => subscribeToPreferredAppearanceChange(themeChange), []);
// Sync appearance when k/v changes
useEffect(() => setAppearance(appearanceKv.value as Appearance), [appearanceKv.value]);
useEffect(() => setAppearance(appearanceKv.value), [appearanceKv.value]);
return {
appearance: appearanceKv.value,

View File

@@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query';
export function useWorkspaces() {
return (
useQuery(['workspaces'], async () => {
console.log('INVOKING WORKSPACES');
const workspaces = (await invoke('workspaces')) as Workspace[];
return workspaces.map(convertDates);
}).data ?? []

3
src-web/lib/clamp.ts Normal file
View File

@@ -0,0 +1,3 @@
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

View File

@@ -0,0 +1,62 @@
import { invoke } from '@tauri-apps/api';
import type { KeyValue } from './models';
const DEFAULT_NAMESPACE = 'app';
type KeyValueValue = string | number | boolean;
export async function setKeyValue<T>({
namespace = DEFAULT_NAMESPACE,
key,
value,
}: {
namespace?: string;
key: string | string[];
value: T;
}): Promise<T> {
await invoke('set_key_value', {
namespace,
key: buildKeyValueKey(key),
value: JSON.stringify(value),
});
return value;
}
export async function getKeyValue<T extends KeyValueValue>({
namespace = DEFAULT_NAMESPACE,
key,
fallback,
}: {
namespace?: string;
key: string | string[];
fallback: T;
}) {
const kv = (await invoke('get_key_value', {
namespace,
key: buildKeyValueKey(key),
})) as KeyValue | null;
return extractKeyValueOrFallback(kv, fallback);
}
export function extractKeyValue<T extends KeyValueValue>(kv: KeyValue | null): T | undefined {
if (kv === null) return undefined;
try {
return JSON.parse(kv.value) as T;
} catch (err) {
return undefined;
}
}
export function extractKeyValueOrFallback<T extends KeyValueValue>(
kv: KeyValue | null,
fallback: T,
): T {
const v = extractKeyValue<T>(kv);
if (v === undefined) return fallback;
return v;
}
export function buildKeyValueKey(key: string | string[]): string {
if (typeof key === 'string') return key;
return key.join('::');
}

View File

@@ -3,7 +3,6 @@ export interface BaseModel {
readonly workspaceId: string;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly deletedAt: Date | null;
}
export interface Workspace extends BaseModel {
@@ -46,14 +45,11 @@ export interface HttpResponse extends BaseModel {
readonly headers: HttpHeader[];
}
export function convertDates<T extends Pick<BaseModel, 'createdAt' | 'updatedAt' | 'deletedAt'>>(
m: T,
): T {
export function convertDates<T extends Pick<BaseModel, 'createdAt' | 'updatedAt'>>(m: T): T {
return {
...m,
createdAt: convertDate(m.createdAt),
updatedAt: convertDate(m.updatedAt),
deletedAt: m.deletedAt ? convertDate(m.deletedAt) : null,
};
}

View File

@@ -1,17 +1,11 @@
import { invoke } from '@tauri-apps/api';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './components/App';
import type { KeyValue } from './lib/models';
import type { Appearance } from './lib/theme/window';
import { getKeyValue } from './lib/keyValueStore';
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
import './main.css';
const appearance: KeyValue = await invoke('get_key_value', {
namespace: 'app',
key: 'appearance',
});
setAppearance((appearance?.value ?? getPreferredAppearance()) as Appearance);
setAppearance(await getKeyValue({ key: 'appearance', fallback: getPreferredAppearance() }));
// root holds our app's root DOM Element:
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(