mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-25 19:01:14 +01:00
JWT auth plugin and necessary updates
This commit is contained in:
@@ -2,19 +2,24 @@ import type { Folder, HttpRequest } from '@yaakapp-internal/models';
|
||||
import type {
|
||||
FormInput,
|
||||
FormInputCheckbox,
|
||||
FormInputEditor,
|
||||
FormInputFile,
|
||||
FormInputHttpRequest,
|
||||
FormInputSelect,
|
||||
FormInputText,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { Input } from './core/Input';
|
||||
import { Label } from './core/Label';
|
||||
import { Select } from './core/Select';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
@@ -41,7 +46,7 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VStack space={3}>
|
||||
{config.map((a, i) => {
|
||||
switch (a.type) {
|
||||
case 'select':
|
||||
@@ -50,7 +55,7 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
key={i + stateKey}
|
||||
arg={a}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] ? String(data[a.name]) : '__ERROR__'}
|
||||
value={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
@@ -64,6 +69,17 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
value={data[a.name] ? String(data[a.name]) : ''}
|
||||
/>
|
||||
);
|
||||
case 'editor':
|
||||
return (
|
||||
<EditorArg
|
||||
key={i}
|
||||
stateKey={stateKey}
|
||||
arg={a}
|
||||
useTemplating={useTemplating || false}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] ? String(data[a.name]) : ''}
|
||||
/>
|
||||
);
|
||||
case 'checkbox':
|
||||
return (
|
||||
<CheckboxArg
|
||||
@@ -93,7 +109,7 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,10 +139,11 @@ function TextArg({
|
||||
onChange={handleChange}
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? '' : value}
|
||||
require={!arg.optional}
|
||||
type={arg.password ? 'password' : 'text'}
|
||||
label={
|
||||
<>
|
||||
{arg.label ?? arg.name}
|
||||
{arg.optional && <span> (optional)</span>}
|
||||
{arg.optional && <span className="text-xs text-text-subtlest"> (optional)</span>}
|
||||
</>
|
||||
}
|
||||
hideLabel={arg.label == null}
|
||||
@@ -138,6 +155,50 @@ function TextArg({
|
||||
);
|
||||
}
|
||||
|
||||
function EditorArg({
|
||||
arg,
|
||||
onChange,
|
||||
value,
|
||||
useTemplating,
|
||||
stateKey,
|
||||
}: {
|
||||
arg: FormInputEditor;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
useTemplating: boolean;
|
||||
stateKey: string;
|
||||
}) {
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
onChange(value === '' ? DYNAMIC_FORM_NULL_ARG : value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const id = `input-${arg.name}`;
|
||||
|
||||
return (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<Label htmlFor={id}>{arg.label}</Label>
|
||||
<Editor
|
||||
id={id}
|
||||
className={classNames(
|
||||
'border border-border rounded-md overflow-hidden px-2 py-1.5',
|
||||
'focus-within:border-border-focus',
|
||||
)}
|
||||
language={arg.language}
|
||||
onChange={handleChange}
|
||||
heightMode="auto"
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? '' : value}
|
||||
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
|
||||
useTemplating={useTemplating}
|
||||
stateKey={stateKey}
|
||||
forceUpdateKey={stateKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectArg({
|
||||
arg,
|
||||
value,
|
||||
@@ -155,7 +216,7 @@ function SelectArg({
|
||||
value={value}
|
||||
options={[
|
||||
...arg.options.map((a) => ({
|
||||
label: a.name + (arg.defaultValue === a.value ? ' (default)' : ''),
|
||||
label: a.name,
|
||||
value: a.value === arg.defaultValue ? DYNAMIC_FORM_NULL_ARG : a.value,
|
||||
})),
|
||||
]}
|
||||
|
||||
@@ -143,8 +143,9 @@ export function GrpcConnectionSetupPane({
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.name,
|
||||
value: a.pluginName,
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
|
||||
@@ -14,7 +14,7 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||
const auths = useHttpAuthentication();
|
||||
const auth = auths.find((a) => a.pluginName === request.authenticationType);
|
||||
const auth = auths.find((a) => a.name === request.authenticationType);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(authentication: Record<string, boolean>) => {
|
||||
|
||||
@@ -235,8 +235,9 @@ export const RequestPane = memo(function RequestPane({
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.name,
|
||||
value: a.pluginName,
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
|
||||
@@ -7,7 +7,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import type { TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
@@ -51,16 +51,7 @@ export interface EditorProps {
|
||||
type?: 'text' | 'password';
|
||||
className?: string;
|
||||
heightMode?: 'auto' | 'full';
|
||||
language?:
|
||||
| 'javascript'
|
||||
| 'json'
|
||||
| 'html'
|
||||
| 'xml'
|
||||
| 'graphql'
|
||||
| 'url'
|
||||
| 'pairs'
|
||||
| 'text'
|
||||
| 'markdown';
|
||||
language?: EditorLanguage | 'pairs';
|
||||
forceUpdateKey?: string | number;
|
||||
autoFocus?: boolean;
|
||||
autoSelect?: boolean;
|
||||
@@ -90,10 +81,6 @@ const stateFields = { history: historyField, folds: foldState };
|
||||
const emptyVariables: EnvironmentVariable[] = [];
|
||||
const emptyExtension: Extension = [];
|
||||
|
||||
// NOTE: For some reason, the cursor doesn't appear if the field is empty and there is no
|
||||
// placeholder. So we set it to a space to force it to show.
|
||||
const emptyPlaceholder = ' ';
|
||||
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
{
|
||||
readOnly,
|
||||
@@ -178,11 +165,11 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
useEffect(
|
||||
function configurePlaceholder() {
|
||||
if (cm.current === null) return;
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder));
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type));
|
||||
const effect = placeholderCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects: effect });
|
||||
},
|
||||
[placeholder],
|
||||
[placeholder, type],
|
||||
);
|
||||
|
||||
// Update vim
|
||||
@@ -354,7 +341,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
const extensions = [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder)),
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '', type)),
|
||||
),
|
||||
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : []),
|
||||
keymapCompartment.current.of(
|
||||
@@ -592,18 +579,21 @@ function getExtensions({
|
||||
];
|
||||
}
|
||||
|
||||
const placeholderElFromText = (text: string) => {
|
||||
const placeholderElFromText = (text: string, type: EditorProps['type']) => {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = text.replaceAll('\n', '<br/>');
|
||||
if (type === 'password') {
|
||||
// Will be obscured (dots) so just needs to be something to take up space
|
||||
el.innerHTML = 'aaaaaaaaaa';
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
function saveCachedEditorState(stateKey: string | null, state: EditorState | null) {
|
||||
if (!stateKey || state == null) return;
|
||||
sessionStorage.setItem(
|
||||
computeFullStateKey(stateKey),
|
||||
JSON.stringify(state.toJSON(stateFields)),
|
||||
);
|
||||
sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(state.toJSON(stateFields)));
|
||||
}
|
||||
|
||||
function getCachedEditorState(doc: string, stateKey: string | null) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { EditorView } from 'codemirror';
|
||||
import type { ReactNode } from 'react';
|
||||
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import { Editor } from './Editor/Editor';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -94,7 +95,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
const id = `input-${label}`;
|
||||
const id = useRef(`input-${generateId()}`);
|
||||
const editorClassName = classNames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder',
|
||||
@@ -140,7 +141,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<Label htmlFor={id} className={classNames(labelClassName, hideLabel && 'sr-only')}>
|
||||
<Label htmlFor={id.current} className={classNames(labelClassName, hideLabel && 'sr-only')}>
|
||||
{label}
|
||||
</Label>
|
||||
<HStack
|
||||
@@ -168,7 +169,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
id={id}
|
||||
id={id.current}
|
||||
singleLine
|
||||
stateKey={stateKey}
|
||||
wrapLines={wrapLines}
|
||||
|
||||
@@ -38,7 +38,6 @@ export function RadioDropdown<T = string | null>({
|
||||
return {
|
||||
key: item.value,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
|
||||
@@ -114,7 +114,7 @@ export function Tabs({
|
||||
}
|
||||
className={btnClassName}
|
||||
>
|
||||
{option && 'shortLabel' in option
|
||||
{option && 'shortLabel' in option && option.shortLabel
|
||||
? option.shortLabel
|
||||
: (option?.label ?? 'Unknown')}
|
||||
{t.rightSlot}
|
||||
|
||||
Reference in New Issue
Block a user