feat(scenarios): add step title, scenario credentials, and environment context in executor

- add nullable title column to scenario steps; exposed in create/edit forms and step table
- add scenario-credential join (many-to-many with CredentialEntity) with CRUD endpoints
- expose environment URLs in code executor helpers via helpers.env and helpers.getEnvUrl()
- resolve and cache environment per run from the login step's environmentName in scheduler
- add section spacing and step table title column to ScenarioDetailPage
This commit is contained in:
2026-04-09 23:37:21 +03:00
parent 1f3a604940
commit 1efbbb38a3
34 changed files with 1362 additions and 14 deletions
@@ -0,0 +1,60 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-family: var(--font-family);
width: 100%;
}
.label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text);
}
.textarea {
font-family: var(--font-family-mono, monospace);
font-size: var(--font-size-sm);
color: var(--color-text);
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
width: 100%;
outline: none;
resize: vertical;
transition: border-color var(--transition), box-shadow var(--transition);
}
.textarea::placeholder {
color: var(--color-text-muted);
}
.textarea:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--color-bg-subtle);
}
.hasError {
border-color: var(--color-error-fg);
}
.hasError:focus {
border-color: var(--color-error-fg);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
}
.hint {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
}
.error {
font-size: var(--font-size-xs);
color: var(--color-error-fg);
}
+40
View File
@@ -0,0 +1,40 @@
import React, { useId } from 'react';
import styles from './Textarea.module.css';
export interface TextareaProps
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'id'> {
label?: string;
hint?: string;
error?: string;
}
export function Textarea({ label, hint, error, className, ...props }: TextareaProps) {
const id = useId();
return (
<div className={styles.wrapper}>
{label && (
<label htmlFor={id} className={styles.label}>
{label}
</label>
)}
<textarea
id={id}
className={[styles.textarea, error ? styles.hasError : '', className]
.filter(Boolean)
.join(' ')}
aria-describedby={error ? `${id}-error` : hint ? `${id}-hint` : undefined}
aria-invalid={error ? true : undefined}
{...props}
/>
{error ? (
<span id={`${id}-error`} className={styles.error} role="alert">
{error}
</span>
) : hint ? (
<span id={`${id}-hint`} className={styles.hint}>
{hint}
</span>
) : null}
</div>
);
}