style(execution): simplify credential authorization UI
This commit is contained in:
parent
e43063579e
commit
a37eacb238
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue