Simple auth schemes

This commit is contained in:
Gregory Schier
2023-03-29 09:03:38 -07:00
parent af9755c513
commit 0f58986b4c
18 changed files with 392 additions and 157 deletions

View File

@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event';
import { appWindow } from '@tauri-apps/api/window';
import { MotionConfig } from 'framer-motion';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@@ -16,11 +17,13 @@ import type { SidebarDisplay } from '../hooks/useSidebarDisplay';
import { sidebarDisplayDefaultValue, sidebarDisplayKey } from '../hooks/useSidebarDisplay';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { debounce } from '../lib/debounce';
import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
import { AppRouter } from './AppRouter';
import { DialogProvider } from './DialogContext';
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';
const UPDATE_DEBOUNCE_MILLIS = 500;
const queryClient = new QueryClient({
defaultOptions: {
@@ -33,7 +36,7 @@ const queryClient = new QueryClient({
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
throttleTime: 1000,
throttleTime: 1000, // 1 second
});
persistQueryClient({
@@ -42,40 +45,47 @@ persistQueryClient({
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
if (keyValue.updatedBy === appWindow.label) return;
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
});
await listen(
'updated_key_value',
debounce(({ payload: keyValue }: { payload: KeyValue }) => {
if (keyValue.updatedBy === appWindow.label) return;
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
}, UPDATE_DEBOUNCE_MILLIS),
);
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
if (request.updatedBy === appWindow.label) return;
await listen(
'updated_request',
debounce(({ payload: request }: { payload: HttpRequest }) => {
if (request.updatedBy === appWindow.label) return;
queryClient.setQueryData(
requestsQueryKey(request.workspaceId),
(requests: HttpRequest[] = []) => {
const newRequests = [];
let found = false;
for (const r of requests) {
if (r.id === request.id) {
found = true;
newRequests.push(request);
} else {
newRequests.push(r);
queryClient.setQueryData(
requestsQueryKey(request.workspaceId),
(requests: HttpRequest[] = []) => {
const newRequests = [];
let found = false;
for (const r of requests) {
if (r.id === request.id) {
found = true;
newRequests.push(request);
} else {
newRequests.push(r);
}
}
}
if (!found) {
newRequests.push(request);
}
return newRequests;
},
);
});
if (!found) {
newRequests.push(request);
}
return newRequests;
},
);
}, UPDATE_DEBOUNCE_MILLIS),
);
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
queryClient.setQueryData(
responsesQueryKey(response.requestId),
(responses: HttpResponse[] = []) => {
if (response.updatedBy === appWindow.label) return;
// We want updates from every response
// if (response.updatedBy === appWindow.label) return;
const newResponses = [];
let found = false;
@@ -95,26 +105,29 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
);
});
await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => {
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
if (workspace.updatedBy === appWindow.label) return;
await listen(
'updated_workspace',
debounce(({ payload: workspace }: { payload: Workspace }) => {
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
if (workspace.updatedBy === appWindow.label) return;
const newWorkspaces = [];
let found = false;
for (const w of workspaces) {
if (w.id === workspace.id) {
found = true;
newWorkspaces.push(workspace);
} else {
newWorkspaces.push(w);
const newWorkspaces = [];
let found = false;
for (const w of workspaces) {
if (w.id === workspace.id) {
found = true;
newWorkspaces.push(workspace);
} else {
newWorkspaces.push(w);
}
}
}
if (!found) {
newWorkspaces.push(workspace);
}
return newWorkspaces;
});
});
if (!found) {
newWorkspaces.push(workspace);
}
return newWorkspaces;
});
}, UPDATE_DEBOUNCE_MILLIS),
);
await listen(
'deleted_model',

View File

@@ -0,0 +1,42 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props {
requestId: string;
authentication: HttpRequest['authentication'];
}
export function BasicAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
return (
<VStack className="my-2" space={2}>
<Input
label="Username"
name="username"
size="sm"
defaultValue={`${authentication.username}`}
onChange={(username: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { password: r.authentication.password, username },
}));
}}
/>
<Input
label="Password"
name="password"
size="sm"
defaultValue={`${authentication.password}`}
onChange={(password: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { username: r.authentication.username, password },
}));
}}
/>
</VStack>
);
}

View File

@@ -0,0 +1,30 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props {
requestId: string;
authentication: HttpRequest['authentication'];
}
export function BearerAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
return (
<VStack className="my-2" space={2}>
<Input
label="Token"
name="token"
size="sm"
defaultValue={`${authentication.token}`}
onChange={(token: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { token },
}));
}}
/>
</VStack>
);
}

View File

@@ -7,7 +7,17 @@ import { useKeyValue } from '../hooks/useKeyValue';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models';
import { BODY_TYPE_GRAPHQL, BODY_TYPE_JSON, BODY_TYPE_NONE, BODY_TYPE_XML } from '../lib/models';
import {
AUTH_TYPE_BASIC,
AUTH_TYPE_BEARER,
AUTH_TYPE_NONE,
BODY_TYPE_GRAPHQL,
BODY_TYPE_JSON,
BODY_TYPE_NONE,
BODY_TYPE_XML,
} from '../lib/models';
import { BasicAuth } from './BasicAuth';
import { BearerAuth } from './BearerAuth';
import { Editor } from './core/Editor';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
@@ -43,10 +53,11 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
options: {
value: activeRequest.bodyType,
items: [
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
{ label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ type: 'separator' },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
],
onChange: async (bodyType) => {
const patch: Partial<HttpRequest> = { bodyType };
@@ -76,21 +87,36 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
options: {
value: activeRequest.authenticationType,
items: [
{ label: 'No Auth', shortLabel: 'Auth', value: null },
{ label: 'Basic', value: 'basic' },
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
],
onChange: async (a) => {
await updateRequest.mutate({
authenticationType: a,
authentication: { username: '', password: '' },
});
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest?.authentication;
if (authenticationType === AUTH_TYPE_BASIC) {
authentication = {
username: authentication.username ?? '',
password: authentication.password ?? '',
};
} else if (authenticationType === AUTH_TYPE_BEARER) {
authentication = {
token: authentication.token ?? '',
};
}
await updateRequest.mutate({ authenticationType, authentication });
},
},
},
{ value: 'params', label: 'URL Params' },
{ value: 'headers', label: 'Headers' },
],
[activeRequest?.bodyType, activeRequest?.headers, activeRequest?.authenticationType],
[
activeRequest?.bodyType,
activeRequest?.headers,
activeRequest?.authenticationType,
activeRequest?.authentication,
],
);
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
@@ -123,24 +149,38 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
label="Request body"
>
<TabContent value="auth">
<div className="flex items-center justify-center min-h-[5rem]">
<header>Hello</header>
</div>
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth
key={forceUpdateKey}
requestId={activeRequest.id}
authentication={activeRequest.authentication}
/>
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
<BearerAuth
key={forceUpdateKey}
requestId={activeRequest.id}
authentication={activeRequest.authentication}
/>
) : (
<EmptyStateText>
No Authentication {activeRequest.authenticationType}
</EmptyStateText>
)}
</TabContent>
<TabContent value="headers">
<HeaderEditor
key={`${activeRequest.id}::${forceUpdateHeaderEditorKey}`}
key={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers}
onChange={handleHeadersChange}
/>
</TabContent>
<TabContent value="params">
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
<ParametersEditor key={forceUpdateKey} parameters={[]} onChange={() => null} />
</TabContent>
<TabContent value="body" className="pl-3 mt-1">
{activeRequest.bodyType === BODY_TYPE_JSON ? (
<Editor
key={activeRequest.id}
key={forceUpdateKey}
useTemplating
placeholder="..."
className="!bg-gray-50"
@@ -152,7 +192,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
/>
) : activeRequest.bodyType === BODY_TYPE_XML ? (
<Editor
key={activeRequest.id}
key={forceUpdateKey}
useTemplating
placeholder="..."
className="!bg-gray-50"
@@ -163,7 +203,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
/>
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
<GraphQLEditor
key={activeRequest.id}
key={forceUpdateKey}
baseRequest={activeRequest}
className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''}

View File

@@ -1,4 +1,3 @@
import { dialog } from '@tauri-apps/api';
import classnames from 'classnames';
import type { ForwardedRef, KeyboardEvent } from 'react';
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
@@ -99,14 +98,15 @@ function SidebarItems({
const shouldUpdateAll = afterPriority - beforePriority < 1;
if (shouldUpdateAll) {
newRequests.forEach((r, i) => {
updateRequest.mutate({ id: r.id, sortPriority: i * 1000 });
newRequests.forEach(({ id }, i) => {
const sortPriority = i * 1000;
const update = (r: HttpRequest) => ({ ...r, sortPriority });
updateRequest.mutate({ id, update });
});
} else {
updateRequest.mutate({
id: requestId,
sortPriority: afterPriority - (afterPriority - beforePriority) / 2,
});
const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
const update = (r: HttpRequest) => ({ ...r, sortPriority });
updateRequest.mutate({ id: requestId, update });
}
},
[hoveredIndex, requests],
@@ -149,7 +149,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => {
await updateRequest.mutate({ name: el.value });
await updateRequest.mutate((r) => ({ ...r, name: el.value }));
setEditing(false);
}, []);

View File

@@ -8,6 +8,11 @@ import { Portal } from '../Portal';
import { Separator } from './Separator';
import { VStack } from './Stacks';
export type DropdownItemSeparator = {
type: 'separator';
label?: string;
};
export type DropdownItem =
| {
type?: 'default';
@@ -18,10 +23,7 @@ export type DropdownItem =
rightSlot?: ReactNode;
onSelect?: () => void;
}
| {
type: 'separator';
label?: string;
};
| DropdownItemSeparator;
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;

View File

@@ -69,7 +69,7 @@ export function Input({
htmlFor={id}
className={classnames(
labelClassName,
'font-semibold text-sm uppercase text-gray-700',
'font-semibold text-xs uppercase text-gray-700',
hideLabel && 'sr-only',
)}
>

View File

@@ -1,30 +1,39 @@
import { useMemo } from 'react';
import type { DropdownProps } from './Dropdown';
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
import { Dropdown } from './Dropdown';
import { Icon } from './Icon';
export interface RadioDropdownItem<T> {
label: string;
shortLabel?: string;
value: T;
}
export type RadioDropdownItem =
| {
type?: 'default';
label: string;
shortLabel?: string;
value: string | null;
}
| DropdownItemSeparator;
export interface RadioDropdownProps<T = string | null> {
value: T;
onChange: (value: T) => void;
items: RadioDropdownItem<T>[];
export interface RadioDropdownProps {
value: string | null;
onChange: (value: string | null) => void;
items: RadioDropdownItem[];
children: DropdownProps['children'];
}
export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) {
export function RadioDropdown({ value, items, onChange, children }: RadioDropdownProps) {
const dropdownItems = useMemo(
() =>
items.map(({ label, shortLabel, value: v }) => ({
label,
shortLabel,
onSelect: () => onChange(v),
leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />,
})),
items.map((item) => {
if (item.type === 'separator') {
return item;
} else {
return {
label: item.label,
shortLabel: item.shortLabel,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
};
}
}),
[value, items],
);

View File

@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
});
type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul';
as?: ComponentType | 'ul' | 'form';
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center';
justifyContent?: 'start' | 'center' | 'end';

View File

@@ -82,7 +82,9 @@ export function Tabs({
isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900',
);
if ('options' in t) {
const option = t.options.items.find((i) => i.value === t.options?.value);
const option = t.options.items.find(
(i) => 'value' in i && i.value === t.options?.value,
);
return (
<RadioDropdown
key={t.value}
@@ -96,7 +98,9 @@ export function Tabs({
onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName}
>
{option?.shortLabel ?? option?.label ?? 'Unknown'}
{option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
<Icon icon="triangleDown" className="-mr-1.5" />
</Button>
</RadioDropdown>