Conditionally disable auth

This commit is contained in:
Gregory Schier
2025-11-26 10:30:16 -08:00
parent dfa6f1c5b4
commit 79dd50474d
11 changed files with 348 additions and 117 deletions

View File

@@ -1,24 +1,19 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { useCallback } from 'react';
import { openFolderSettings } from '../commands/openFolderSettings';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Checkbox } from './core/Checkbox';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Input, type InputProps } from './core/Input';
import { Link } from './core/Link';
import { SegmentedControl } from './core/SegmentedControl';
import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText';
@@ -36,7 +31,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
);
const handleChange = useCallback(
async (authentication: Record<string, boolean>) => await patchModel(model, { authentication }),
async (authentication: Record<string, unknown>) => await patchModel(model, { authentication }),
[model],
);
@@ -98,30 +93,65 @@ export function HttpAuthenticationEditor({ model }: Props) {
}
return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<HStack space={2} className="mb-2" alignItems="center">
<Checkbox
className="w-full"
checked={!model.authentication.disabled}
onChange={(disabled) => handleChange({ ...model.authentication, disabled: !disabled })}
title="Enabled"
/>
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
<Dropdown
items={authConfig.data.actions.map(
(a): DropdownItem => ({
label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(model),
}),
)}
>
<IconButton title="Authentication Actions" icon="settings" size="xs" />
</Dropdown>
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-3">
<div>
<HStack space={2} alignItems="start">
<SegmentedControl
label="Enabled"
hideLabel
name="enabled"
value={
model.authentication.disabled === false || model.authentication.disabled == null
? '__TRUE__'
: model.authentication.disabled === true
? '__FALSE__'
: '__DYNAMIC__'
}
options={[
{ label: 'Enabled', value: '__TRUE__' },
{ label: 'Disabled', value: '__FALSE__' },
{ label: 'Enabled when...', value: '__DYNAMIC__' },
]}
onChange={async (enabled) => {
let disabled: boolean | string;
if (enabled === '__TRUE__') {
disabled = false;
} else if (enabled === '__FALSE__') {
disabled = true;
} else {
disabled = '';
}
console.log('SETTING DISABLED', disabled);
await handleChange({ ...model.authentication, disabled });
}}
/>
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
<Dropdown
items={authConfig.data.actions.map(
(a): DropdownItem => ({
label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(model),
}),
)}
>
<IconButton title="Authentication Actions" icon="settings" size="xs" />
</Dropdown>
)}
</HStack>
{typeof model.authentication.disabled === 'string' && (
<div className="mt-3">
<AuthenticationDisabledInput
className="w-full"
stateKey={`auth.${model.id}.dynamic`}
value={model.authentication.disabled}
onChange={(v) => handleChange({ ...model.authentication, disabled: v })}
/>
</div>
)}
</HStack>
</div>
<DynamicForm
disabled={model.authentication.disabled}
disabled={model.authentication.disabled === true}
autocompleteVariables
autocompleteFunctions
stateKey={`auth.${model.id}.${model.authenticationType}`}
@@ -132,3 +162,45 @@ export function HttpAuthenticationEditor({ model }: Props) {
</div>
);
}
function AuthenticationDisabledInput({
value,
onChange,
stateKey,
className,
}: {
value: string;
onChange: InputProps['onChange'];
stateKey: string;
className?: string;
}) {
const rendered = useRenderTemplate({
template: value,
enabled: true,
purpose: 'preview',
ignoreError: true,
refreshKey: value,
});
return (
<Input
size="sm"
className={className}
label="Dynamic Disabled"
hideLabel
defaultValue={value}
placeholder="Enabled when this renders a non-empty value"
rightSlot={
<div className="px-1 flex items-center">
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
{rendered.isPending ? 'loading' : rendered.data ? 'enabled' : 'disabled'}
</div>
</div>
}
autocompleteFunctions
autocompleteVariables
onChange={onChange}
stateKey={stateKey}
/>
);
}

View File

@@ -67,8 +67,10 @@ export function MarkdownEditor({
<div className="absolute top-0 right-0 pt-1.5 pr-1.5">
<SegmentedControl
name={name}
label="View mode"
onChange={setViewMode}
value={viewMode}
className="opacity-0 group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100"
options={[
{ icon: 'eye', label: 'Preview mode', value: 'preview' },
{ icon: 'pencil', label: 'Edit mode', value: 'edit' },

View File

@@ -132,12 +132,13 @@ function InitializedTemplateFunctionDialog({
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
const [renderKey, setRenderKey] = useState<string | null>(null);
const rendered = useRenderTemplate(
debouncedTagText,
previewType !== 'none',
previewType === 'click' ? 'send' : 'preview',
previewType === 'live' ? renderKey + debouncedTagText : renderKey,
);
const rendered = useRenderTemplate({
template: debouncedTagText,
enabled: previewType !== 'none',
purpose: previewType === 'click' ? 'send' : 'preview',
refreshKey: previewType === 'live' ? renderKey + debouncedTagText : renderKey,
ignoreError: false,
});
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
// biome-ignore lint/correctness/useExhaustiveDependencies: Only update this on rendered data change to keep secrets hidden on input change

View File

@@ -1,75 +1,122 @@
import classNames from 'classnames';
import { useRef } from 'react';
import { type ReactNode, useRef } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { generateId } from '../../lib/generateId';
import { Button } from './Button';
import type { IconProps } from './Icon';
import { IconButton } from './IconButton';
import { IconButton, type IconButtonProps } from './IconButton';
import { Label } from './Label';
import { HStack } from './Stacks';
interface Props<T extends string> {
options: { value: T; label: string; icon: IconProps['icon'] }[];
options: { value: T; label: string; icon?: IconProps['icon'] }[];
onChange: (value: T) => void;
value: T;
name: string;
size?: IconButtonProps['size'];
label: string;
className?: string;
hideLabel?: boolean;
labelClassName?: string;
help?: ReactNode;
}
export function SegmentedControl<T extends string>({
value,
onChange,
options,
size = 'xs',
label,
hideLabel,
labelClassName,
help,
className,
}: Props<T>) {
const [selectedValue, setSelectedValue] = useStateWithDeps<T>(value, [value]);
const containerRef = useRef<HTMLDivElement>(null);
const id = useRef(`input-${generateId()}`);
return (
<HStack
ref={containerRef}
role="group"
dir="ltr"
space={0.5}
className={classNames(
className,
'bg-surface-highlight rounded-md mb-auto opacity-0',
'transition-opacity transform-gpu',
'group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100',
)}
onKeyDown={(e) => {
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
if (e.key === 'ArrowRight') {
const newIndex = Math.abs((selectedIndex + 1) % options.length);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
} else if (e.key === 'ArrowLeft') {
const newIndex = Math.abs((selectedIndex - 1) % options.length);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
}
}}
>
{options.map((o) => {
const isSelected = selectedValue === o.value;
const isActive = value === o.value;
return (
<IconButton
size="xs"
variant="solid"
color={isActive ? 'secondary' : undefined}
role="radio"
tabIndex={isSelected ? 0 : -1}
className={classNames(
isActive && '!text-text',
'!px-1.5 !w-auto',
'focus:ring-border-focus',
)}
key={o.label}
title={o.label}
icon={o.icon}
onClick={() => onChange(o.value)}
/>
);
})}
</HStack>
<div className="w-full grid">
<Label
htmlFor={id.current}
help={help}
visuallyHidden={hideLabel}
className={classNames(labelClassName)}
>
{label}
</Label>
<HStack
id={id.current}
ref={containerRef}
role="group"
dir="ltr"
space={1}
className={classNames(
className,
'bg-surface-highlight rounded-lg mb-auto mr-auto',
'transition-opacity transform-gpu p-1',
)}
onKeyDown={(e) => {
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
if (e.key === 'ArrowRight') {
e.preventDefault();
const newIndex = Math.abs((selectedIndex + 1) % options.length);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const newIndex = Math.abs((selectedIndex - 1) % options.length);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
}
}}
>
{options.map((o) => {
const isSelected = selectedValue === o.value;
const isActive = value === o.value;
if (o.icon == null) {
return (
<Button
key={o.label}
size={size}
variant="solid"
color={isActive ? 'secondary' : undefined}
role="radio"
tabIndex={isSelected ? 0 : -1}
className={classNames(
isActive && '!text-text',
'focus:ring-1 focus:ring-border-focus',
)}
onClick={() => onChange(o.value)}
>
{o.label}
</Button>
);
} else {
return (
<IconButton
key={o.label}
size={size}
variant="solid"
color={isActive ? 'secondary' : undefined}
role="radio"
tabIndex={isSelected ? 0 : -1}
className={classNames(
isActive && '!text-text',
'!px-1.5 !w-auto',
'focus:ring-border-focus',
)}
title={o.label}
icon={o.icon}
onClick={() => onChange(o.value)}
/>
);
}
})}
</HStack>
</div>
);
}

View File

@@ -90,6 +90,7 @@ export function Select<T extends string>({
onBlur={() => setFocused(false)}
className={classNames(
'pr-7 w-full outline-none bg-transparent disabled:opacity-disabled',
'leading-[1]', // Center the text better vertically
)}
disabled={disabled}
>