style(execution): simplify credential authorization UI

This commit is contained in:
Lucio Lelii 2026-09-30 10:42:15 +02:00
parent e43063579e
commit a37eacb238
3 changed files with 133 additions and 35 deletions

View File

@ -1013,3 +1013,66 @@
align-items: center;
gap: 4px;
}
/* Credential setup uses the same neutral surface and compact controls as the execution view. */
.execution-credential-banner,
.execution-credential-card {
border: 1px solid #dbe3ee;
border-radius: .5rem;
background: #fff;
}
.execution-credential-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
margin: .25rem;
padding: .7rem .9rem;
border-left: 3px solid #3b82f6;
}
.execution-credential-banner__title {
display: flex;
align-items: center;
gap: .375rem;
color: #334155;
font-size: .8rem;
font-weight: 700;
}
.execution-credential-banner__message { margin-top: .125rem; font-size: .75rem; color: #64748b; }
.execution-credential-card {
margin: .75rem;
padding: .8rem;
}
.execution-credential-card--provided {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
border-left: 3px solid #22c55e;
}
.execution-credential-card__title { color: #334155; font-size: .8125rem; font-weight: 700; }
.execution-credential-button {
border: 1px solid transparent;
border-radius: .375rem;
padding: .45rem .7rem;
font-size: .75rem;
font-weight: 600;
cursor: pointer;
}
.execution-credential-button--primary { background: #2563eb; color: #fff; }
.execution-credential-button--secondary { border-color: #cbd5e1; background: #fff; color: #334155; }
.execution-credential-button:disabled { opacity: .55; }
.execution-credential-card__error {
margin-top: .6rem;
padding: .5rem .625rem;
border: 1px solid #fecaca;
border-radius: .375rem;
background: #fef2f2;
color: #b91c1c;
font-size: .75rem;
}

View File

@ -124,16 +124,24 @@
</div>
}
@if (authorizationGate().missingProviders.length || authorizationGate().runtime.length) {
<div class="mx-1 mt-1 flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900">
<span>
<section class="execution-credential-banner font-sans">
<div class="execution-credential-banner__copy">
<div class="execution-credential-banner__title">
<mat-icon fontIcon="key"></mat-icon>
<span>Authorization required</span>
</div>
<div class="execution-credential-banner__message">
@if (authorizationGate().missingProviders.length) {
This execution needs a Vault credential for {{ authorizationGate().missingProviders.join(', ') }} before it can start.
} @else {
This execution needs a provider authorization before it can start.
}
</span>
<button type="button" mat-stroked-button (click)="openAuthorizationPanel()">Choose credential</button>
</div>
</div>
</div>
<button type="button" class="execution-credential-button execution-credential-button--primary" (click)="openAuthorizationAction()">
{{ authorizationActionLabel() }}
</button>
</section>
}
@if (outcomes().length) {
<div class="execution-outcomes">
@ -343,58 +351,64 @@
}
@for (entry of settledVaultAuthorizations(); track entry.requirement.key) {
<section class="m-3 flex items-center justify-between gap-3 rounded-md border border-emerald-200 bg-emerald-50 p-3">
<section class="execution-credential-card execution-credential-card--provided font-sans">
<div class="min-w-0">
<div class="text-xs font-semibold text-slate-800">{{ entry.provider }} credential</div>
<div class="mt-1 truncate text-[11px] text-slate-600">{{ providedCredentialLabel(entry) }}</div>
<div class="execution-credential-card__title">{{ entry.provider }} credential</div>
<div class="mt-1 text-xs text-slate-500">{{ providedCredentialLabel(entry) }}</div>
</div>
<button type="button" mat-stroked-button (click)="changeVaultAuthorization(entry)">Change</button>
<button type="button" class="execution-credential-button execution-credential-button--secondary" (click)="changeVaultAuthorization(entry)">Change</button>
</section>
}
@for (entry of pendingVaultAuthorizations(); track entry.requirement.key) {
<section class="m-3 rounded-md border border-amber-200 bg-amber-50 p-3">
<div class="text-xs font-semibold text-slate-800">Vault credential for {{ entry.provider }}</div>
<div class="mt-1 text-[11px] text-slate-600">Select an active credential to start the execution.</div>
<section class="execution-credential-card font-sans">
<div class="execution-credential-card__title">Vault credential for {{ entry.provider }}</div>
<div class="mt-1 text-xs text-slate-500">Select an active credential to start the execution.</div>
@if (entry.requiresCredential === false) {
<div class="mt-1 text-[11px] text-amber-800">This provider is listed as not requiring a credential, but the execution is asking for one.</div>
<div class="mt-2 text-xs text-amber-700">This provider is listed as not requiring a credential, but the execution is asking for one.</div>
}
<mat-form-field appearance="outline" class="mt-2 w-full">
<mat-label>Credential</mat-label>
<mat-select
@if (llmCredentialLoadingFor(entry)) {
<div class="mt-2 text-xs text-slate-500">Loading saved credentials…</div>
} @else if (llmCredentialOptionsFor(entry).length) {
<label class="block mt-3 text-xs font-semibold text-slate-600">
<span>Credential</span>
<select
class="block w-full mt-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-slate-700"
[value]="selectedLlmCredentialFor(entry)"
[disabled]="llmCredentialLoadingFor(entry) || authorizationSavingFor(entry) || !llmCredentialOptionsFor(entry).length"
(selectionChange)="selectLlmCredential(entry, $event.value)">
[disabled]="authorizationSavingFor(entry)"
(change)="selectLlmCredential(entry, $any($event.target).value)">
<option value="" disabled>Select a credential</option>
@for (credential of llmCredentialOptionsFor(entry); track credential.id) {
<mat-option [value]="credential.id">
<option [value]="credential.id">
{{ credential.label }}{{ credential.description ? ' — ' + credential.description : '' }}
</mat-option>
</option>
}
</mat-select>
@if (llmCredentialLoadingFor(entry)) { <mat-hint>Loading credentials...</mat-hint> }
@else if (authorizationSavingFor(entry)) { <mat-hint>Saving credential...</mat-hint> }
@else if (!llmCredentialOptionsFor(entry).length) { <mat-hint>No credentials available</mat-hint> }
</mat-form-field>
</select>
</label>
@if (authorizationSavingFor(entry)) { <div class="mt-2 text-xs text-slate-500">Saving credential…</div> }
} @else {
<div class="mt-2 rounded-md bg-slate-50 px-3 py-2 text-xs text-slate-500">No saved {{ entry.provider }} credentials. Add one to continue.</div>
}
@if (llmCredentialErrorFor(entry); as credentialError) {
<div class="mb-2 flex items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">
<div class="execution-credential-card__error">
<span>{{ credentialError }}</span>
<button type="button" mat-stroked-button (click)="retryLlmCredentials(entry)">Retry</button>
<button type="button" class="execution-credential-button execution-credential-button--secondary" (click)="retryLlmCredentials(entry)">Retry</button>
</div>
}
@if (authorizationErrorFor(entry); as authorizationError) {
<div class="mb-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">{{ authorizationError }}</div>
<div class="execution-credential-card__error">{{ authorizationError }}</div>
}
@if (credentialFormError(); as formError) {
<div class="mb-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">{{ formError }}</div>
<div class="execution-credential-card__error">{{ formError }}</div>
}
<div class="mt-2 flex flex-wrap gap-2">
<div class="mt-3 flex flex-wrap gap-2">
@if (!llmCredentialLoadingFor(entry)) {
<button type="button" mat-stroked-button [disabled]="credentialFormSaving()" (click)="openVaultCredentialForm(entry.provider)">
<button type="button" class="execution-credential-button execution-credential-button--primary" [disabled]="credentialFormSaving()" (click)="openVaultCredentialForm(entry.provider)">
{{ credentialFormSaving() ? 'Saving...' : 'Add credential' }}
</button>
}
@if (editingAuthorizationKeys()[entry.requirement.key]) {
<button type="button" mat-stroked-button (click)="cancelVaultAuthorizationChange(entry)">Cancel</button>
<button type="button" class="execution-credential-button execution-credential-button--secondary" (click)="cancelVaultAuthorizationChange(entry)">Cancel</button>
}
</div>
</section>

View File

@ -10,8 +10,6 @@ import { NotificationService } from '@services/notifications/notification';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import {
areFlowValueKindsCompatible,
FlowBlock,
@ -143,7 +141,7 @@ function uploadFailureMessage(failure: unknown): string {
@Component({
selector: 'app-task-execution-viewer',
imports: [CommonModule, FormsModule, ReteEditor, TaskExecutionInputsPanelComponent, MatButtonModule, MatIconModule, MatTooltipModule, MatFormFieldModule, MatSelectModule, BiasImpactReportListComponent, JsonViewerComponent],
imports: [CommonModule, FormsModule, ReteEditor, TaskExecutionInputsPanelComponent, MatButtonModule, MatIconModule, MatTooltipModule, BiasImpactReportListComponent, JsonViewerComponent],
templateUrl: './task-execution-viewer.html',
styleUrl: './task-execution-viewer.css',
changeDetection: ChangeDetectionStrategy.OnPush
@ -966,6 +964,29 @@ export class TaskExecutionViewerComponent implements OnDestroy {
this.activeAsideTab.set('inputs');
}
/** The banner creates the first credential directly: there is nothing to choose from yet. */
openAuthorizationAction() {
const pending = this.pendingVaultAuthorizations();
if (pending.length === 1
&& !this.llmCredentialLoadingFor(pending[0])
&& !this.llmCredentialOptionsFor(pending[0]).length) {
this.openAuthorizationPanel();
void this.openVaultCredentialForm(pending[0].provider);
return;
}
this.openAuthorizationPanel();
}
authorizationActionLabel(): string {
const pending = this.pendingVaultAuthorizations();
if (pending.length === 1
&& !this.llmCredentialLoadingFor(pending[0])
&& !this.llmCredentialOptionsFor(pending[0]).length) {
return 'Add credential';
}
return pending.length ? 'Choose credential' : 'Open authorization';
}
toggleContextAside() {
this.contextAsideOpen.update((open) => !open);
}