Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.270"
VERSION = "0.261.271"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
117 changes: 117 additions & 0 deletions application/v2_ui/src/components/ui/SectionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SectionCard.tsx
// The distinct card V2 Admin Settings draws every section in, for surfaces outside Admin.
//
// Admin's `SettingsSection` owns its card markup inline, alongside schema-driven fields this
// component knows nothing about. The workflow editor needs the same object -- a header band
// with the section's icon, a title large enough to find while scrolling, a line of facts, and
// the Admin status chip -- around controls it lays out itself. The classes match Admin's card
// (bar one narrow-width allowance, noted below), so a section here and a section there read as
// the same thing, and so `.admin-field` rows inside resolve their container queries against
// this card the way they do in Admin.
//
// The title takes focus (`tabIndex={-1}`) so an "On this page" jump can land a keyboard or
// screen reader user on the card it scrolled to, as Admin's index does.

import { clsx } from 'clsx';
import type { LucideIcon } from 'lucide-react';
import type { ReactNode } from 'react';
import type { SectionStatus } from '../../lib/adminSections';
import { presentSectionStatus } from '../admin/sectionStatusPresentation';

export function SectionCard({
id,
title,
icon: Icon,
meta,
status = 'none',
actions,
ariaLabel,
headingLevel = 2,
className,
bodyClassName,
children,
...data
}: {
/** The card's DOM id. The title is `${id}-title`, which an index jump focuses. */
id: string;
title: ReactNode;
icon: LucideIcon;
/** One line of facts under the title. */
meta?: ReactNode;
status?: SectionStatus;
/** Controls drawn in the header band beside the status chip. */
actions?: ReactNode;
/**
* Names the region instead of its visible title, for a card whose established accessible
* name differs from the title it now shows.
*/
ariaLabel?: string;
headingLevel?: 2 | 3;
className?: string;
bodyClassName?: string;
children?: ReactNode;
[dataAttribute: `data-${string}`]: string | undefined;
}) {
const titleId = `${id}-title`;
const presentation = presentSectionStatus(status);
const Heading = headingLevel === 3 ? 'h3' : 'h2';

return (
<section
id={id}
aria-label={ariaLabel}
aria-labelledby={ariaLabel ? undefined : titleId}
className={clsx(
'glass glass-edge rounded-2xl',
'admin-settings-distinct scroll-mt-4 border-edge-strong',
className,
)}
{...data}
>
<div className="flex flex-wrap items-start justify-between gap-3 rounded-t-2xl border-b border-edge-strong bg-surface-2 p-4 sm:px-5">
{/* Admin's header, except that the title column asks for 15rem before the chip may
sit beside it, so a narrow card wraps the chip rather than breaking the facts
line mid-word. */}
<div className="flex min-w-0 flex-1 basis-60 flex-wrap items-start gap-3">
<span
aria-hidden="true"

Check warning on line 77 in application/v2_ui/src/components/ui/SectionCard.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-edge-strong bg-surface-solid text-text-2"
>
<Icon size={20} />
</span>
<div className="min-w-0 flex-1 basis-40">
<Heading
id={titleId}
tabIndex={-1}
className="text-lg leading-snug font-semibold text-text-1"
>
{title}
</Heading>
{meta ? <p className="mt-1 text-xs text-text-3">{meta}</p> : null}
</div>
</div>

{presentation || actions ? (
<div className="flex max-w-full min-w-0 flex-wrap items-center gap-2">
{presentation ? (
// Free to shrink once it has wrapped onto its own line, so a long
// status at a large text size wraps inside the card instead of past it.
<span
className={clsx(
'flex max-w-full min-w-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs',
presentation.className,
)}
>
<presentation.Icon size={11} aria-hidden="true" className="shrink-0" />

Check warning on line 105 in application/v2_ui/src/components/ui/SectionCard.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
{presentation.label}
</span>
) : null}
{actions}
</div>
) : null}
</div>

<div className={clsx('admin-section-body p-4 sm:p-5', bodyClassName)}>{children}</div>
</section>
);
}
167 changes: 94 additions & 73 deletions application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
// Native alert authoring for personal and group workflows: when to alert, and the rules that decide it.

import { useId } from 'react';
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
import { ArrowDown, ArrowUp, BellRing, Plus, Trash2 } from 'lucide-react';
import { GlassButton, Toggle } from '../ui/primitives';
import { SectionCard } from '../ui/SectionCard';
import { Pill } from '../workspace/primitives';
import {
WorkflowCardHelp,
WorkflowField,
WorkflowFieldEmphasis,
WorkflowFieldList,
workflowFieldInputClass,
type WorkflowCardFrame,
} from './WorkflowField';
import {
describeWorkflowAlertCondition,
newWorkflowAlertRule,
Expand Down Expand Up @@ -167,15 +176,18 @@
return text && allowed.includes(text) ? text : fallback;
}

function Select({ label, value, options, onChange, disabled }: {
function Select({ id, label, value, options, onChange, disabled, className }: {

Check warning on line 179 in application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
id?: string;
label: string;
value: string;
options: Option[];
onChange: (value: string) => void;
disabled?: boolean;
/** Replaces the rule-row field styling, for a select drawn in a card's field row. */
className?: string;
}) {
return (
<select className={fieldClass} aria-label={label} value={value} disabled={disabled}
<select id={id} className={className ?? fieldClass} aria-label={label} value={value} disabled={disabled}

Check warning on line 190 in application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
onChange={(event) => onChange(event.target.value)}>
{withCurrent(options, value).map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
Expand Down Expand Up @@ -508,10 +520,13 @@
export function WorkflowAlertEditor({
workflow,
scope,
card,
onChange,
}: {
workflow: WorkflowDefinition;
scope: WorkflowScope;
/** The Alerts card's place in the editor and how it reads at a glance. */
card: WorkflowCardFrame;
onChange: (update: (workflow: WorkflowDefinition) => WorkflowDefinition) => void;
}) {
const titleId = useId();
Expand Down Expand Up @@ -548,80 +563,86 @@
return key;
});
const atLimit = rules.length >= WORKFLOW_ALERT_MAX_RULES;
const leadsSettings = mode === 'every_run' || mode === 'rules' || rules.length > 0 || usesModel;

return (
<section aria-labelledby={titleId} className="space-y-4 rounded-2xl border border-edge p-4">
<div>
<h3 id={titleId} className="text-base font-semibold text-text-1">Alerts</h3>
<p className="mt-0.5 max-w-prose text-xs text-text-3">
Alerts notify you when a run meets a condition you define. A run that matches nothing stays silent.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<label className="min-w-0 text-sm text-text-2">
When to alert
<Select label="When to alert" value={mode} options={MODE_OPTIONS}
<SectionCard id={card.id} title="Alerts" icon={BellRing} status={card.status} meta={card.meta}
headingLevel={card.headingLevel}>
<WorkflowCardHelp>
Alerts notify you when a run meets a condition you define. A run that matches nothing stays silent.
</WorkflowCardHelp>
<WorkflowFieldList>
<WorkflowField label="When to alert" htmlFor={`${titleId}-mode`} width="standard">
<Select id={`${titleId}-mode`} label="When to alert" value={mode} options={MODE_OPTIONS}
className={workflowFieldInputClass}
onChange={(next) => edit((settings) => ({ ...settings, alert_mode: next }))} />
</label>
{mode === 'every_run' ? (
<label className="min-w-0 text-sm text-text-2">
Pop-up alert priority
<Select label="Pop-up alert priority" value={priority} options={PRIORITY_OPTIONS}
onChange={(next) => edit((settings) => ({ ...settings, alert_priority: next }))} />
</label>
) : null}
</div>
{mode === 'rules' || rules.length ? (
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<h4 className="text-sm font-semibold text-text-1">Alert rules</h4>
{mode === 'rules' ? (
<GlassButton type="button" size="sm" disabled={atLimit}
onClick={() => editRules((current) => [...current, newWorkflowAlertRule()])}>
<Plus size={14} /> Add alert rule
</GlassButton>
</WorkflowField>
</WorkflowFieldList>
{leadsSettings ? (
<WorkflowFieldEmphasis emphasis="dependent">
<WorkflowFieldList>
{mode === 'every_run' ? (
<WorkflowField label="Pop-up alert priority" htmlFor={`${titleId}-priority`} width="standard">
<Select id={`${titleId}-priority`} label="Pop-up alert priority" value={priority}
options={PRIORITY_OPTIONS} className={workflowFieldInputClass}
onChange={(next) => edit((settings) => ({ ...settings, alert_priority: next }))} />
</WorkflowField>
) : null}
</div>
{mode !== 'rules' ? (
<p className="max-w-prose text-xs text-text-3">
These saved rules send alerts only when you choose “Only when a condition is met”. They are kept, and still checked, when you save.
</p>
) : null}
{mode === 'rules' && !rules.length ? (
<p className="text-sm text-text-3">No alert rules yet. Add a rule to choose what should notify you.</p>
) : null}
{mode === 'rules' && atLimit ? (
<p role="status" className="text-xs text-text-3">A workflow can have up to {WORKFLOW_ALERT_MAX_RULES} alert rules.</p>
) : null}
{rules.length ? (
<ol aria-label="Alert rules" className="list-none space-y-4">
{rules.map((rule, index) => (
<AlertRuleRow key={ruleKeys[index]}
rule={rule} position={index + 1} count={rules.length} tasks={tasks}
error={errors.get(index + 1)} workflowScope={workflowScope} soundsEnabled={soundsEnabled}
onChange={(next) => editRules((current) => current.map((item, itemIndex) => itemIndex === index ? next : item))}
onMove={(direction) => moveRule(index, direction)}
onRemove={() => editRules((current) => current.filter((_, itemIndex) => itemIndex !== index))} />
))}
</ol>
) : null}
{mode === 'rules' && rules.length ? (
<p className="max-w-prose text-xs text-text-3">
When several rules match the same run, the highest severity wins and every matched rule is listed in the alert.
Info and low severities go to the notification bell only. Medium and above open the pop-up alert.
</p>
) : null}
</div>
) : null}
{usesModel ? (
<label className="block max-w-md text-sm text-text-2">
If a model evaluated condition cannot be judged
<Select label="If a model evaluated condition cannot be judged" value={onError} options={ON_ERROR_OPTIONS}
onChange={(next) => edit((settings) => ({
...settings, alert_evaluation: { ...record(settings.alert_evaluation), on_error: next },
}))} />
</label>
{mode === 'rules' || rules.length ? (
<div className="space-y-3 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<h4 className="text-sm font-semibold text-text-1">Alert rules</h4>
{mode === 'rules' ? (
<GlassButton type="button" size="sm" disabled={atLimit}

Check warning on line 596 in application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
onClick={() => editRules((current) => [...current, newWorkflowAlertRule()])}>
<Plus size={14} /> Add alert rule
</GlassButton>
) : null}
</div>
{mode !== 'rules' ? (
<p className="max-w-prose text-xs text-text-3">
These saved rules send alerts only when you choose “Only when a condition is met”. They are kept, and still checked, when you save.
</p>
) : null}
{mode === 'rules' && !rules.length ? (
<p className="text-sm text-text-3">No alert rules yet. Add a rule to choose what should notify you.</p>
) : null}
{mode === 'rules' && atLimit ? (
<p role="status" className="text-xs text-text-3">A workflow can have up to {WORKFLOW_ALERT_MAX_RULES} alert rules.</p>

Check warning on line 611 in application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
) : null}
{rules.length ? (
<ol aria-label="Alert rules" className="list-none space-y-4">
{rules.map((rule, index) => (
<AlertRuleRow key={ruleKeys[index]}
rule={rule} position={index + 1} count={rules.length} tasks={tasks}
error={errors.get(index + 1)} workflowScope={workflowScope} soundsEnabled={soundsEnabled}
onChange={(next) => editRules((current) => current.map((item, itemIndex) => itemIndex === index ? next : item))}
onMove={(direction) => moveRule(index, direction)}
onRemove={() => editRules((current) => current.filter((_, itemIndex) => itemIndex !== index))} />
))}
</ol>
) : null}
{mode === 'rules' && rules.length ? (
<p className="max-w-prose text-xs text-text-3">
When several rules match the same run, the highest severity wins and every matched rule is listed in the alert.
Info and low severities go to the notification bell only. Medium and above open the pop-up alert.
</p>
) : null}
</div>
) : null}
{usesModel ? (
<WorkflowField label="If a model evaluated condition cannot be judged" htmlFor={`${titleId}-on-error`}
width="standard">
<Select id={`${titleId}-on-error`} label="If a model evaluated condition cannot be judged"
value={onError} options={ON_ERROR_OPTIONS} className={workflowFieldInputClass}
onChange={(next) => edit((settings) => ({
...settings, alert_evaluation: { ...record(settings.alert_evaluation), on_error: next },
}))} />
</WorkflowField>
) : null}
</WorkflowFieldList>
</WorkflowFieldEmphasis>
) : null}
</section>
</SectionCard>
);
}
Loading
Loading