refactor(scenario): remove step type and simplify scheduler to exec-only
- remove StepType, type column, and type field from step entity and DTOs - remove executeLoginStep, executeSignStep, executeExecStep from scheduler - inline exec logic directly in executeStepRun; all steps run execCode - remove AuthService, EnvironmentService, runEnvironments from scheduler - remove type selector from CreateStepPage and EditStepPage - remove type badge column from ScenarioDetailPage - fix useEffect dependency arrays in RunDetailPage (FINAL, id, runId, polling) - fix duplicate /snippets proxy key in vite.config.ts - remove unused escapeHtml export from hljs.ts
This commit is contained in:
@@ -83,7 +83,10 @@ export const snippets = {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
update(id: string, patch: Partial<Pick<Snippet, 'name' | 'description' | 'code'>>): Promise<Snippet> {
|
||||
update(
|
||||
id: string,
|
||||
patch: Partial<Pick<Snippet, 'name' | 'description' | 'code'>>,
|
||||
): Promise<Snippet> {
|
||||
return request(`/snippets/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
@@ -184,7 +187,11 @@ export const scenarios = {
|
||||
waitForRun(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
|
||||
return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' });
|
||||
},
|
||||
listRuns(scenarioId: string, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
||||
listRuns(
|
||||
scenarioId: string,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
||||
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
|
||||
},
|
||||
exportScenario(id: string): Promise<unknown> {
|
||||
@@ -201,12 +208,24 @@ export const scenarios = {
|
||||
// ── Runs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const runs = {
|
||||
listAll(page = 1, limit = 20, status?: string): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: string; name: string } }>> {
|
||||
listAll(
|
||||
page = 1,
|
||||
limit = 20,
|
||||
status?: string,
|
||||
): Promise<
|
||||
PaginatedResponse<
|
||||
ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: string; name: string } }
|
||||
>
|
||||
> {
|
||||
const q = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
if (status) q.set('status', status);
|
||||
return request(`/scenarios/runs?${q}`);
|
||||
},
|
||||
list(scenarioId: string, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
||||
list(
|
||||
scenarioId: string,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
||||
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
|
||||
},
|
||||
get(scenarioId: string, runId: string, q?: string): Promise<ScenarioRunDetail> {
|
||||
@@ -238,7 +257,6 @@ export const scenarioCredentials = {
|
||||
export interface CreateStepPayload {
|
||||
title?: string;
|
||||
order: number;
|
||||
type: 'login' | 'exec' | 'sign';
|
||||
execCode?: string;
|
||||
validateCode?: string;
|
||||
}
|
||||
@@ -246,7 +264,6 @@ export interface CreateStepPayload {
|
||||
export interface UpdateStepPayload {
|
||||
title?: string;
|
||||
order?: number;
|
||||
type?: 'login' | 'exec' | 'sign';
|
||||
execCode?: string;
|
||||
validateCode?: string;
|
||||
}
|
||||
|
||||
@@ -68,13 +68,10 @@ export interface Session {
|
||||
|
||||
// ── Scenarios ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export type StepType = 'login' | 'exec' | 'sign';
|
||||
|
||||
export interface ScenarioStep {
|
||||
id: string;
|
||||
scenarioId: string;
|
||||
order: number;
|
||||
type: StepType;
|
||||
title: string | null;
|
||||
sessionName: string | null;
|
||||
execCode: string | null;
|
||||
|
||||
@@ -6,7 +6,3 @@ hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('json', json);
|
||||
|
||||
export default hljs;
|
||||
|
||||
export function escapeHtml(str: string): string {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
@@ -6,7 +6,15 @@ import { CodeBlock } from '../../ui';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
import { credentials } from '../../api';
|
||||
import type { Credential } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
DescriptionList,
|
||||
Notification,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function CredentialDetailPage() {
|
||||
@@ -95,11 +103,7 @@ export function CredentialDetailPage() {
|
||||
{ term: t('credentials.field_id'), detail: <UuidBadge id={credential.id} /> },
|
||||
{
|
||||
term: t('credentials.field_last_used'),
|
||||
detail: credential.lastUsedAt ? (
|
||||
<Timestamp value={credential.lastUsedAt} />
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
detail: credential.lastUsedAt ? <Timestamp value={credential.lastUsedAt} /> : '—',
|
||||
},
|
||||
{
|
||||
term: t('credentials.field_created'),
|
||||
|
||||
@@ -5,7 +5,15 @@ import { Settings, Pencil, Trash2, Plus, Download } from 'lucide-react';
|
||||
import { parse as yamlParse } from 'yaml';
|
||||
import { credentials } from '../../api';
|
||||
import type { Credential } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, ContextMenu, DescriptionList, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
ContextMenu,
|
||||
DescriptionList,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
function CredentialCard({
|
||||
|
||||
@@ -4,7 +4,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Settings, ExternalLink, Pencil, Trash2, Plus } from 'lucide-react';
|
||||
import { environments } from '../../api';
|
||||
import type { Environment } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, ContextMenu, DescriptionList, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
ContextMenu,
|
||||
DescriptionList,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
|
||||
|
||||
@@ -3,7 +3,14 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { runs } from '../../api';
|
||||
import type { ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
||||
import { AutoRefreshIndicator, Badge, Table, Timestamp, UuidBadge, type TableColumn } from '../../ui';
|
||||
import {
|
||||
AutoRefreshIndicator,
|
||||
Badge,
|
||||
Table,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
type TableColumn,
|
||||
} from '../../ui';
|
||||
import type { BadgeVariant } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
@@ -37,6 +44,7 @@ export function AllRunsPage() {
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
runs
|
||||
.listAll()
|
||||
.then((res) => {
|
||||
@@ -48,7 +56,7 @@ export function AllRunsPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
load();
|
||||
pollRef.current = setInterval(load, 10_000);
|
||||
return () => {
|
||||
@@ -77,9 +85,7 @@ export function AllRunsPage() {
|
||||
key: 'status',
|
||||
header: t('runs.col_status'),
|
||||
width: 110,
|
||||
render: (r) => (
|
||||
<Badge variant={STATUS_VARIANT[r.status]}>{STATUS_LABEL[r.status]}</Badge>
|
||||
),
|
||||
render: (r) => <Badge variant={STATUS_VARIANT[r.status]}>{STATUS_LABEL[r.status]}</Badge>,
|
||||
},
|
||||
{
|
||||
key: 'steps',
|
||||
|
||||
@@ -58,6 +58,8 @@ const LOG_LEVEL_VARIANT: Record<LogLevel, BadgeVariant> = {
|
||||
error: 'error',
|
||||
};
|
||||
|
||||
const FINAL: ScenarioRunStatus[] = ['pass', 'fail'];
|
||||
|
||||
export function RunDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id, runId } = useParams<{ id: string; runId: string }>();
|
||||
@@ -76,8 +78,6 @@ export function RunDetailPage() {
|
||||
const LOG_PAGE_SIZE = 25;
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const FINAL: ScenarioRunStatus[] = ['pass', 'fail'];
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollRef.current) {
|
||||
clearInterval(pollRef.current);
|
||||
@@ -97,8 +97,11 @@ export function RunDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !runId || polling) return;
|
||||
runs.get(id, runId, debouncedSearch).then(setRun).catch(() => undefined);
|
||||
}, [debouncedSearch]);
|
||||
runs
|
||||
.get(id, runId, debouncedSearch)
|
||||
.then(setRun)
|
||||
.catch(() => undefined);
|
||||
}, [id, runId, polling, debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !runId) return;
|
||||
@@ -124,8 +127,12 @@ export function RunDetailPage() {
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => { if (!cancelled) setError(err.message); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
.catch((err: Error) => {
|
||||
if (!cancelled) setError(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -4,7 +4,16 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Play } from 'lucide-react';
|
||||
import { scenarios, runs } from '../../api';
|
||||
import type { Scenario, ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api';
|
||||
import { AutoRefreshIndicator, Badge, Breadcrumbs, Button, Table, Timestamp, UuidBadge, type TableColumn } from '../../ui';
|
||||
import {
|
||||
AutoRefreshIndicator,
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Table,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
type TableColumn,
|
||||
} from '../../ui';
|
||||
import type { BadgeVariant } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
@@ -50,6 +59,7 @@ export function RunsPage() {
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
load();
|
||||
pollRef.current = setInterval(load, 10_000);
|
||||
return () => {
|
||||
@@ -68,9 +78,7 @@ export function RunsPage() {
|
||||
key: 'status',
|
||||
header: t('runs.col_status'),
|
||||
width: 100,
|
||||
render: (r) => (
|
||||
<Badge variant={STATUS_VARIANT[r.status]}>{STATUS_LABEL[r.status]}</Badge>
|
||||
),
|
||||
render: (r) => <Badge variant={STATUS_VARIANT[r.status]}>{STATUS_LABEL[r.status]}</Badge>,
|
||||
},
|
||||
{
|
||||
key: 'steps',
|
||||
|
||||
@@ -2,13 +2,11 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { scenarios, steps } from '../../api';
|
||||
import type { Scenario, StepType } from '../../api';
|
||||
import type { Scenario } from '../../api';
|
||||
import type { CreateStepPayload } from '../../api/client';
|
||||
import { Breadcrumbs, Button, Card, Input, Select, CodeEditor } from '../../ui';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
const STEP_TYPES: StepType[] = ['login', 'exec', 'sign'];
|
||||
|
||||
export function CreateStepPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -17,7 +15,6 @@ export function CreateStepPage() {
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [order, setOrder] = useState('0');
|
||||
const [title, setTitle] = useState('');
|
||||
const [type, setType] = useState<StepType>('exec');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [validateCode, setValidateCode] = useState('');
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof CreateStepPayload, string>>>({});
|
||||
@@ -26,7 +23,10 @@ export function CreateStepPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
scenarios.get(id).then(setScenario).catch(() => null);
|
||||
scenarios
|
||||
.get(id)
|
||||
.then(setScenario)
|
||||
.catch(() => null);
|
||||
}, [id]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
@@ -45,7 +45,6 @@ export function CreateStepPage() {
|
||||
await steps.create(id!, {
|
||||
order: Number(order),
|
||||
title: title.trim() || undefined,
|
||||
type,
|
||||
execCode: execCode.trim() || undefined,
|
||||
validateCode: validateCode.trim() || undefined,
|
||||
});
|
||||
@@ -91,32 +90,22 @@ export function CreateStepPage() {
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
label={t('steps.form_type')}
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as StepType)}
|
||||
options={STEP_TYPES.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
rows={6}
|
||||
<CodeEditor
|
||||
value={execCode}
|
||||
onChange={(e) => setExecCode(e.target.value)}
|
||||
onChange={setExecCode}
|
||||
rows={6}
|
||||
placeholder={t('steps.form_exec_code_placeholder')}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('steps.form_validate_code')}</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
rows={6}
|
||||
<CodeEditor
|
||||
value={validateCode}
|
||||
onChange={(e) => setValidateCode(e.target.value)}
|
||||
onChange={setValidateCode}
|
||||
rows={6}
|
||||
placeholder={t('steps.form_validate_code_placeholder')}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,10 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { scenarios, steps } from '../../api';
|
||||
import type { Scenario, ScenarioStep, StepType } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, Select, CodeEditor } from '../../ui';
|
||||
import type { Scenario, ScenarioStep } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
const STEP_TYPES: StepType[] = ['login', 'exec', 'sign'];
|
||||
|
||||
export function EditStepPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id, stepId } = useParams<{ id: string; stepId: string }>();
|
||||
@@ -17,7 +15,6 @@ export function EditStepPage() {
|
||||
const [step, setStep] = useState<ScenarioStep | null>(null);
|
||||
const [order, setOrder] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [type, setType] = useState<StepType>('exec');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [validateCode, setValidateCode] = useState('');
|
||||
const [orderError, setOrderError] = useState('');
|
||||
@@ -33,7 +30,6 @@ export function EditStepPage() {
|
||||
setStep(st);
|
||||
setOrder(String(st.order));
|
||||
setTitle(st.title ?? '');
|
||||
setType(st.type);
|
||||
setExecCode(st.execCode ?? '');
|
||||
setValidateCode(st.validateCode ?? '');
|
||||
})
|
||||
@@ -59,7 +55,6 @@ export function EditStepPage() {
|
||||
await steps.update(id!, stepId!, {
|
||||
order: Number(order),
|
||||
title: title.trim() || undefined,
|
||||
type,
|
||||
execCode: execCode.trim() || undefined,
|
||||
validateCode: validateCode.trim() || undefined,
|
||||
});
|
||||
@@ -110,27 +105,13 @@ export function EditStepPage() {
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
label={t('steps.form_type')}
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as StepType)}
|
||||
options={STEP_TYPES.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
|
||||
<CodeEditor
|
||||
value={execCode}
|
||||
onChange={setExecCode}
|
||||
rows={6}
|
||||
/>
|
||||
<CodeEditor value={execCode} onChange={setExecCode} rows={6} />
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('steps.form_validate_code')}</label>
|
||||
<CodeEditor
|
||||
value={validateCode}
|
||||
onChange={setValidateCode}
|
||||
rows={6}
|
||||
/>
|
||||
<CodeEditor value={validateCode} onChange={setValidateCode} rows={6} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { stringify as yamlStringify } from 'yaml';
|
||||
import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api';
|
||||
import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api';
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
@@ -21,12 +20,6 @@ import {
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
const STEP_TYPE_VARIANT: Record<string, 'info' | 'warning' | 'success'> = {
|
||||
login: 'success',
|
||||
exec: 'info',
|
||||
sign: 'warning',
|
||||
};
|
||||
|
||||
export function ScenarioDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -95,8 +88,14 @@ export function ScenarioDetailPage() {
|
||||
const handleAddCredential = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
let valid = true;
|
||||
if (!addCredId) { setAddCredError(t('scenarios.cred_form_cred_required')); valid = false; }
|
||||
if (!addAlias.trim()) { setAddAliasError(t('scenarios.cred_form_alias_required')); valid = false; }
|
||||
if (!addCredId) {
|
||||
setAddCredError(t('scenarios.cred_form_cred_required'));
|
||||
valid = false;
|
||||
}
|
||||
if (!addAlias.trim()) {
|
||||
setAddAliasError(t('scenarios.cred_form_alias_required'));
|
||||
valid = false;
|
||||
}
|
||||
if (!valid) return;
|
||||
setAddCredSaving(true);
|
||||
try {
|
||||
@@ -122,16 +121,13 @@ export function ScenarioDetailPage() {
|
||||
|
||||
const stepColumns: TableColumn<ScenarioStep>[] = [
|
||||
{ key: 'order', header: t('scenarios.step_order'), render: (s) => s.order, width: 60 },
|
||||
{
|
||||
key: 'type',
|
||||
header: t('scenarios.step_type'),
|
||||
width: 90,
|
||||
render: (s) => <Badge variant={STEP_TYPE_VARIANT[s.type] ?? 'neutral'}>{s.type}</Badge>,
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
header: t('scenarios.step_title'),
|
||||
render: (s) => s.title ?? <span style={{ color: 'var(--color-text-muted)', fontStyle: 'italic' }}>{'—'}</span>,
|
||||
render: (s) =>
|
||||
s.title ?? (
|
||||
<span style={{ color: 'var(--color-text-muted)', fontStyle: 'italic' }}>{'—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'updated',
|
||||
@@ -315,42 +311,52 @@ export function ScenarioDetailPage() {
|
||||
|
||||
{addCredOpen && (
|
||||
<div style={{ marginBottom: 'var(--space-4)' }}>
|
||||
<Card className={styles.formCard}>
|
||||
<form onSubmit={handleAddCredential} noValidate>
|
||||
<div className={styles.formFields}>
|
||||
<Select
|
||||
label={t('scenarios.cred_form_cred')}
|
||||
value={addCredId}
|
||||
onChange={(e) => { setAddCredId(e.target.value); setAddCredError(''); }}
|
||||
error={addCredError || undefined}
|
||||
options={[
|
||||
{ value: '', label: t('scenarios.cred_form_cred_placeholder') },
|
||||
...allCredentials.map((c) => ({ value: String(c.id), label: c.name })),
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={t('scenarios.cred_form_alias')}
|
||||
placeholder={t('scenarios.cred_form_alias_placeholder')}
|
||||
value={addAlias}
|
||||
onChange={(e) => { setAddAlias(e.target.value); setAddAliasError(''); }}
|
||||
error={addAliasError || undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formActions}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => { setAddCredOpen(false); setAddCredId(''); setAddAlias(''); }}
|
||||
>
|
||||
{t('scenarios.cred_action_cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm" loading={addCredSaving}>
|
||||
{t('scenarios.cred_action_save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card className={styles.formCard}>
|
||||
<form onSubmit={handleAddCredential} noValidate>
|
||||
<div className={styles.formFields}>
|
||||
<Select
|
||||
label={t('scenarios.cred_form_cred')}
|
||||
value={addCredId}
|
||||
onChange={(e) => {
|
||||
setAddCredId(e.target.value);
|
||||
setAddCredError('');
|
||||
}}
|
||||
error={addCredError || undefined}
|
||||
options={[
|
||||
{ value: '', label: t('scenarios.cred_form_cred_placeholder') },
|
||||
...allCredentials.map((c) => ({ value: String(c.id), label: c.name })),
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={t('scenarios.cred_form_alias')}
|
||||
placeholder={t('scenarios.cred_form_alias_placeholder')}
|
||||
value={addAlias}
|
||||
onChange={(e) => {
|
||||
setAddAlias(e.target.value);
|
||||
setAddAliasError('');
|
||||
}}
|
||||
error={addAliasError || undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formActions}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAddCredOpen(false);
|
||||
setAddCredId('');
|
||||
setAddAlias('');
|
||||
}}
|
||||
>
|
||||
{t('scenarios.cred_action_cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm" loading={addCredSaving}>
|
||||
{t('scenarios.cred_action_save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,7 +4,16 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { sessions } from '../../api';
|
||||
import type { Session } from '../../api';
|
||||
import { Badge, Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
DescriptionList,
|
||||
Notification,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function SessionDetailPage() {
|
||||
|
||||
@@ -4,7 +4,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { sessions } from '../../api';
|
||||
import type { Session } from '../../api';
|
||||
import { Badge, Breadcrumbs, Button, Table, type TableColumn, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Table,
|
||||
type TableColumn,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function SessionsPage() {
|
||||
|
||||
@@ -121,11 +121,7 @@ export function EditSnippetPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.formActions}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/snippets/${id}`)}
|
||||
>
|
||||
<Button type="button" variant="secondary" onClick={() => navigate(`/snippets/${id}`)}>
|
||||
{t('snippets.action_cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving}>
|
||||
|
||||
@@ -6,7 +6,15 @@ import { CodeBlock } from '../../ui';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
import { snippets } from '../../api';
|
||||
import type { Snippet } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
Card,
|
||||
DescriptionList,
|
||||
Notification,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function SnippetDetailPage() {
|
||||
|
||||
@@ -8,13 +8,7 @@ import type { Snippet } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, ContextMenu, Timestamp, UuidBadge } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
function SnippetCard({
|
||||
snippet,
|
||||
onDelete,
|
||||
}: {
|
||||
snippet: Snippet;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
function SnippetCard({ snippet, onDelete }: { snippet: Snippet; onDelete: (id: string) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -62,9 +56,7 @@ function SnippetCard({
|
||||
footer={footer}
|
||||
onClick={() => navigate(`/snippets/${snippet.id}`)}
|
||||
>
|
||||
{snippet.description && (
|
||||
<p className={styles.muted}>{snippet.description}</p>
|
||||
)}
|
||||
{snippet.description && <p className={styles.muted}>{snippet.description}</p>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -148,9 +140,7 @@ export function SnippetsPage() {
|
||||
{items.map((s) => (
|
||||
<SnippetCard key={s.id} snippet={s} onDelete={handleDelete} />
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<p className={styles.muted}>{t('snippets.empty')}</p>
|
||||
)}
|
||||
{items.length === 0 && <p className={styles.muted}>{t('snippets.empty')}</p>}
|
||||
<AddSnippetCard />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,11 @@ export interface AutoRefreshIndicatorProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function AutoRefreshIndicator({ active, pulseKey, label = 'Live' }: AutoRefreshIndicatorProps) {
|
||||
export function AutoRefreshIndicator({
|
||||
active,
|
||||
pulseKey,
|
||||
label = 'Live',
|
||||
}: AutoRefreshIndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
className={[styles.root, active ? styles.active : styles.inactive].join(' ')}
|
||||
@@ -15,9 +19,7 @@ export function AutoRefreshIndicator({ active, pulseKey, label = 'Live' }: AutoR
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className={styles.dot}>
|
||||
{pulseKey !== undefined && pulseKey > 0 && (
|
||||
<span key={pulseKey} className={styles.ring} />
|
||||
)}
|
||||
{pulseKey !== undefined && pulseKey > 0 && <span key={pulseKey} className={styles.ring} />}
|
||||
</span>
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import hljs, { escapeHtml } from '../../lib/hljs';
|
||||
import hljs from '../../lib/hljs';
|
||||
import styles from './CodeBlock.module.css';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
|
||||
@@ -12,13 +12,13 @@ export interface CodeEditorProps {
|
||||
|
||||
function useMonacoTheme() {
|
||||
const [theme, setTheme] = useState(() =>
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
setTheme(
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light',
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
@@ -49,11 +49,7 @@ export function CodeEditor({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
styles.wrapper,
|
||||
focused ? styles.focused : '',
|
||||
error ? styles.hasError : '',
|
||||
]
|
||||
className={[styles.wrapper, focused ? styles.focused : '', error ? styles.hasError : '']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{ height }}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useId } from 'react';
|
||||
import styles from './Textarea.module.css';
|
||||
|
||||
export interface TextareaProps
|
||||
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'id'> {
|
||||
export interface TextareaProps extends Omit<
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
'id'
|
||||
> {
|
||||
label?: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
|
||||
@@ -13,7 +13,8 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/environments': 'http://localhost:13000',
|
||||
'/credentials': 'http://localhost:13000', '/snippets': 'http://localhost:13000', '/snippets': 'http://localhost:13000',
|
||||
'/credentials': 'http://localhost:13000',
|
||||
'/snippets': 'http://localhost:13000',
|
||||
'/sessions': 'http://localhost:13000',
|
||||
'/scenarios': 'http://localhost:13000',
|
||||
'/keys': 'http://localhost:13000',
|
||||
|
||||
Reference in New Issue
Block a user