mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 17:18:32 +02:00
Conditionally disable auth
This commit is contained in:
@@ -131,7 +131,6 @@ export const plugin: PluginDefinition = {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'grantType',
|
name: 'grantType',
|
||||||
label: 'Grant Type',
|
label: 'Grant Type',
|
||||||
hideLabel: true,
|
|
||||||
defaultValue: defaultGrantType,
|
defaultValue: defaultGrantType,
|
||||||
options: grantTypes,
|
options: grantTypes,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ async fn cmd_render_template<R: Runtime>(
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
purpose: Option<RenderPurpose>,
|
purpose: Option<RenderPurpose>,
|
||||||
|
ignore_error: bool,
|
||||||
) -> YaakResult<String> {
|
) -> YaakResult<String> {
|
||||||
let environment_chain =
|
let environment_chain =
|
||||||
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
|
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
|
||||||
@@ -130,7 +131,11 @@ async fn cmd_render_template<R: Runtime>(
|
|||||||
purpose.unwrap_or(RenderPurpose::Preview),
|
purpose.unwrap_or(RenderPurpose::Preview),
|
||||||
),
|
),
|
||||||
&RenderOptions {
|
&RenderOptions {
|
||||||
error_behavior: RenderErrorBehavior::Throw,
|
error_behavior: if ignore_error {
|
||||||
|
RenderErrorBehavior::ReturnEmpty
|
||||||
|
} else {
|
||||||
|
RenderErrorBehavior::Throw
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use log::info;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||||
@@ -5,7 +6,7 @@ use yaak_models::models::{
|
|||||||
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
||||||
};
|
};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions, TemplateCallback};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
pub async fn render_template<T: TemplateCallback>(
|
pub async fn render_template<T: TemplateCallback>(
|
||||||
template: &str,
|
template: &str,
|
||||||
@@ -45,10 +46,37 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut authentication = BTreeMap::new();
|
let authentication = {
|
||||||
for (k, v) in r.authentication.clone() {
|
let mut disabled = false;
|
||||||
authentication.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
let mut auth = BTreeMap::new();
|
||||||
}
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?;
|
let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?;
|
||||||
|
|
||||||
@@ -99,10 +127,37 @@ pub async fn render_http_request<T: TemplateCallback>(
|
|||||||
body.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
body.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut authentication = BTreeMap::new();
|
let authentication = {
|
||||||
for (k, v) in r.authentication.clone() {
|
let mut disabled = false;
|
||||||
authentication.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
let mut auth = BTreeMap::new();
|
||||||
}
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
let url = parse_and_render(r.url.clone().as_str(), vars, cb, &opt).await?;
|
let url = parse_and_render(r.url.clone().as_str(), vars, cb, &opt).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ impl EncryptionManager {
|
|||||||
let raw_key = mkey
|
let raw_key = mkey
|
||||||
.decrypt(decoded_key.as_slice())
|
.decrypt(decoded_key.as_slice())
|
||||||
.map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
|
.map_err(|e| WorkspaceKeyDecryptionError(e.to_string()))?;
|
||||||
info!("Got existing workspace key for {workspace_id}");
|
|
||||||
let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());
|
let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());
|
||||||
|
|
||||||
Ok(wkey)
|
Ok(wkey)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use yaak_models::models::{Environment, HttpRequestHeader, HttpUrlParameter, WebsocketRequest};
|
use yaak_models::models::{Environment, HttpRequestHeader, HttpUrlParameter, WebsocketRequest};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions, TemplateCallback};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
pub async fn render_websocket_request<T: TemplateCallback>(
|
pub async fn render_websocket_request<T: TemplateCallback>(
|
||||||
r: &WebsocketRequest,
|
r: &WebsocketRequest,
|
||||||
@@ -32,10 +34,37 @@ pub async fn render_websocket_request<T: TemplateCallback>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut authentication = BTreeMap::new();
|
let authentication = {
|
||||||
for (k, v) in r.authentication.clone() {
|
let mut disabled = false;
|
||||||
authentication.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
let mut auth = BTreeMap::new();
|
||||||
}
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?;
|
let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import type {
|
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from '@yaakapp-internal/models';
|
||||||
Folder,
|
|
||||||
GrpcRequest,
|
|
||||||
HttpRequest,
|
|
||||||
WebsocketRequest,
|
|
||||||
Workspace,
|
|
||||||
} from '@yaakapp-internal/models';
|
|
||||||
import { patchModel } from '@yaakapp-internal/models';
|
import { patchModel } from '@yaakapp-internal/models';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { openFolderSettings } from '../commands/openFolderSettings';
|
import { openFolderSettings } from '../commands/openFolderSettings';
|
||||||
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
|
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
|
||||||
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
|
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
|
||||||
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
|
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
|
||||||
|
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import { Checkbox } from './core/Checkbox';
|
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
||||||
import type { DropdownItem } from './core/Dropdown';
|
|
||||||
import { Dropdown } from './core/Dropdown';
|
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { Input, type InputProps } from './core/Input';
|
||||||
import { Link } from './core/Link';
|
import { Link } from './core/Link';
|
||||||
|
import { SegmentedControl } from './core/SegmentedControl';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { DynamicForm } from './DynamicForm';
|
import { DynamicForm } from './DynamicForm';
|
||||||
import { EmptyStateText } from './EmptyStateText';
|
import { EmptyStateText } from './EmptyStateText';
|
||||||
@@ -36,7 +31,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
async (authentication: Record<string, boolean>) => await patchModel(model, { authentication }),
|
async (authentication: Record<string, unknown>) => await patchModel(model, { authentication }),
|
||||||
[model],
|
[model],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -98,30 +93,65 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-3">
|
||||||
<HStack space={2} className="mb-2" alignItems="center">
|
<div>
|
||||||
<Checkbox
|
<HStack space={2} alignItems="start">
|
||||||
className="w-full"
|
<SegmentedControl
|
||||||
checked={!model.authentication.disabled}
|
label="Enabled"
|
||||||
onChange={(disabled) => handleChange({ ...model.authentication, disabled: !disabled })}
|
hideLabel
|
||||||
title="Enabled"
|
name="enabled"
|
||||||
/>
|
value={
|
||||||
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
|
model.authentication.disabled === false || model.authentication.disabled == null
|
||||||
<Dropdown
|
? '__TRUE__'
|
||||||
items={authConfig.data.actions.map(
|
: model.authentication.disabled === true
|
||||||
(a): DropdownItem => ({
|
? '__FALSE__'
|
||||||
label: a.label,
|
: '__DYNAMIC__'
|
||||||
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
|
}
|
||||||
onSelect: () => a.call(model),
|
options={[
|
||||||
}),
|
{ label: 'Enabled', value: '__TRUE__' },
|
||||||
)}
|
{ label: 'Disabled', value: '__FALSE__' },
|
||||||
>
|
{ label: 'Enabled when...', value: '__DYNAMIC__' },
|
||||||
<IconButton title="Authentication Actions" icon="settings" size="xs" />
|
]}
|
||||||
</Dropdown>
|
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
|
<DynamicForm
|
||||||
disabled={model.authentication.disabled}
|
disabled={model.authentication.disabled === true}
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
autocompleteFunctions
|
autocompleteFunctions
|
||||||
stateKey={`auth.${model.id}.${model.authenticationType}`}
|
stateKey={`auth.${model.id}.${model.authenticationType}`}
|
||||||
@@ -132,3 +162,45 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
</div>
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,8 +67,10 @@ export function MarkdownEditor({
|
|||||||
<div className="absolute top-0 right-0 pt-1.5 pr-1.5">
|
<div className="absolute top-0 right-0 pt-1.5 pr-1.5">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
name={name}
|
name={name}
|
||||||
|
label="View mode"
|
||||||
onChange={setViewMode}
|
onChange={setViewMode}
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
|
className="opacity-0 group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100"
|
||||||
options={[
|
options={[
|
||||||
{ icon: 'eye', label: 'Preview mode', value: 'preview' },
|
{ icon: 'eye', label: 'Preview mode', value: 'preview' },
|
||||||
{ icon: 'pencil', label: 'Edit mode', value: 'edit' },
|
{ icon: 'pencil', label: 'Edit mode', value: 'edit' },
|
||||||
|
|||||||
@@ -132,12 +132,13 @@ function InitializedTemplateFunctionDialog({
|
|||||||
|
|
||||||
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
|
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
|
||||||
const [renderKey, setRenderKey] = useState<string | null>(null);
|
const [renderKey, setRenderKey] = useState<string | null>(null);
|
||||||
const rendered = useRenderTemplate(
|
const rendered = useRenderTemplate({
|
||||||
debouncedTagText,
|
template: debouncedTagText,
|
||||||
previewType !== 'none',
|
enabled: previewType !== 'none',
|
||||||
previewType === 'click' ? 'send' : 'preview',
|
purpose: previewType === 'click' ? 'send' : 'preview',
|
||||||
previewType === 'live' ? renderKey + debouncedTagText : renderKey,
|
refreshKey: previewType === 'live' ? renderKey + debouncedTagText : renderKey,
|
||||||
);
|
ignoreError: false,
|
||||||
|
});
|
||||||
|
|
||||||
const tooLarge = rendered.data ? rendered.data.length > 10000 : 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
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Only update this on rendered data change to keep secrets hidden on input change
|
||||||
|
|||||||
@@ -1,75 +1,122 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRef } from 'react';
|
import { type ReactNode, useRef } from 'react';
|
||||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||||
|
import { generateId } from '../../lib/generateId';
|
||||||
|
import { Button } from './Button';
|
||||||
import type { IconProps } from './Icon';
|
import type { IconProps } from './Icon';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton, type IconButtonProps } from './IconButton';
|
||||||
|
import { Label } from './Label';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
interface Props<T extends string> {
|
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;
|
onChange: (value: T) => void;
|
||||||
value: T;
|
value: T;
|
||||||
name: string;
|
name: string;
|
||||||
|
size?: IconButtonProps['size'];
|
||||||
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hideLabel?: boolean;
|
||||||
|
labelClassName?: string;
|
||||||
|
help?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SegmentedControl<T extends string>({
|
export function SegmentedControl<T extends string>({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
|
size = 'xs',
|
||||||
|
label,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
help,
|
||||||
className,
|
className,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const [selectedValue, setSelectedValue] = useStateWithDeps<T>(value, [value]);
|
const [selectedValue, setSelectedValue] = useStateWithDeps<T>(value, [value]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const id = useRef(`input-${generateId()}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<div className="w-full grid">
|
||||||
ref={containerRef}
|
<Label
|
||||||
role="group"
|
htmlFor={id.current}
|
||||||
dir="ltr"
|
help={help}
|
||||||
space={0.5}
|
visuallyHidden={hideLabel}
|
||||||
className={classNames(
|
className={classNames(labelClassName)}
|
||||||
className,
|
>
|
||||||
'bg-surface-highlight rounded-md mb-auto opacity-0',
|
{label}
|
||||||
'transition-opacity transform-gpu',
|
</Label>
|
||||||
'group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100',
|
<HStack
|
||||||
)}
|
id={id.current}
|
||||||
onKeyDown={(e) => {
|
ref={containerRef}
|
||||||
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
|
role="group"
|
||||||
if (e.key === 'ArrowRight') {
|
dir="ltr"
|
||||||
const newIndex = Math.abs((selectedIndex + 1) % options.length);
|
space={1}
|
||||||
options[newIndex] && setSelectedValue(options[newIndex].value);
|
className={classNames(
|
||||||
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
className,
|
||||||
child.focus();
|
'bg-surface-highlight rounded-lg mb-auto mr-auto',
|
||||||
} else if (e.key === 'ArrowLeft') {
|
'transition-opacity transform-gpu p-1',
|
||||||
const newIndex = Math.abs((selectedIndex - 1) % options.length);
|
)}
|
||||||
options[newIndex] && setSelectedValue(options[newIndex].value);
|
onKeyDown={(e) => {
|
||||||
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
|
||||||
child.focus();
|
if (e.key === 'ArrowRight') {
|
||||||
}
|
e.preventDefault();
|
||||||
}}
|
const newIndex = Math.abs((selectedIndex + 1) % options.length);
|
||||||
>
|
options[newIndex] && setSelectedValue(options[newIndex].value);
|
||||||
{options.map((o) => {
|
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
||||||
const isSelected = selectedValue === o.value;
|
child.focus();
|
||||||
const isActive = value === o.value;
|
} else if (e.key === 'ArrowLeft') {
|
||||||
return (
|
e.preventDefault();
|
||||||
<IconButton
|
const newIndex = Math.abs((selectedIndex - 1) % options.length);
|
||||||
size="xs"
|
options[newIndex] && setSelectedValue(options[newIndex].value);
|
||||||
variant="solid"
|
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
||||||
color={isActive ? 'secondary' : undefined}
|
child.focus();
|
||||||
role="radio"
|
}
|
||||||
tabIndex={isSelected ? 0 : -1}
|
}}
|
||||||
className={classNames(
|
>
|
||||||
isActive && '!text-text',
|
{options.map((o) => {
|
||||||
'!px-1.5 !w-auto',
|
const isSelected = selectedValue === o.value;
|
||||||
'focus:ring-border-focus',
|
const isActive = value === o.value;
|
||||||
)}
|
if (o.icon == null) {
|
||||||
key={o.label}
|
return (
|
||||||
title={o.label}
|
<Button
|
||||||
icon={o.icon}
|
key={o.label}
|
||||||
onClick={() => onChange(o.value)}
|
size={size}
|
||||||
/>
|
variant="solid"
|
||||||
);
|
color={isActive ? 'secondary' : undefined}
|
||||||
})}
|
role="radio"
|
||||||
</HStack>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function Select<T extends string>({
|
|||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'pr-7 w-full outline-none bg-transparent disabled:opacity-disabled',
|
'pr-7 w-full outline-none bg-transparent disabled:opacity-disabled',
|
||||||
|
'leading-[1]', // Center the text better vertically
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,20 +6,33 @@ import { invokeCmd } from '../lib/tauri';
|
|||||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||||
|
|
||||||
export function useRenderTemplate(
|
export function useRenderTemplate({
|
||||||
template: string,
|
template,
|
||||||
enabled: boolean,
|
enabled,
|
||||||
purpose: RenderPurpose,
|
purpose,
|
||||||
refreshKey: string | null,
|
refreshKey,
|
||||||
) {
|
ignoreError,
|
||||||
|
preservePreviousValue,
|
||||||
|
}: {
|
||||||
|
template: string;
|
||||||
|
enabled: boolean;
|
||||||
|
purpose: RenderPurpose;
|
||||||
|
refreshKey?: string | null;
|
||||||
|
ignoreError?: boolean;
|
||||||
|
preservePreviousValue?: boolean;
|
||||||
|
}) {
|
||||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
||||||
const environmentId = useActiveEnvironment()?.id ?? null;
|
const environmentId = useActiveEnvironment()?.id ?? null;
|
||||||
return useQuery<string>({
|
return useQuery<string>({
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
enabled,
|
enabled,
|
||||||
queryKey: ['render_template', workspaceId, environmentId, refreshKey, purpose],
|
placeholderData: preservePreviousValue ? (prev) => prev : undefined,
|
||||||
|
queryKey: ['render_template', workspaceId, environmentId, refreshKey, purpose, ignoreError],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
minPromiseMillis(renderTemplate({ template, workspaceId, environmentId, purpose }), 300),
|
minPromiseMillis(
|
||||||
|
renderTemplate({ template, workspaceId, environmentId, purpose, ignoreError }),
|
||||||
|
300,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,13 +41,21 @@ export async function renderTemplate({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environmentId,
|
environmentId,
|
||||||
purpose,
|
purpose,
|
||||||
|
ignoreError,
|
||||||
}: {
|
}: {
|
||||||
template: string;
|
template: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environmentId: string | null;
|
environmentId: string | null;
|
||||||
purpose: RenderPurpose;
|
purpose: RenderPurpose;
|
||||||
|
ignoreError?: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId, purpose });
|
return invokeCmd('cmd_render_template', {
|
||||||
|
template,
|
||||||
|
workspaceId,
|
||||||
|
environmentId,
|
||||||
|
purpose,
|
||||||
|
ignoreError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptTemplate({
|
export async function decryptTemplate({
|
||||||
|
|||||||
Reference in New Issue
Block a user