import { LitElement, html, css, unsafeCSS } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { router } from '../../router';
import { getFlowExecutions } from '../../api';
import { AuthedElement } from '../../api';
import { unifiedWebSocketManager } from '../../services/unified-websocket-manager';
import '@shoelace-style/shoelace/dist/components/badge/badge.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
import '@shoelace-style/shoelace/dist/components/select/select.js';
import '@shoelace-style/shoelace/dist/components/option/option.js';
import { parseUTCDate, formatLocalDateTime } from '../../utils/date';
import consoleStyles from '../../styles/console-styles.css?inline';
import '../../components/view-header.ts';
interface FlowExecution {
id: string;
flow_id: string;
flow_name?: string;
status: string;
start_time: string;
end_time?: string;
actions_taken_summary?: any[];
}
@customElement('flow-executions-view')
export class FlowExecutionsView extends AuthedElement {
static styles = [
unsafeCSS(consoleStyles),
css`
:host {
display: block;
}
.table-wrapper {
overflow-x: auto;
margin-top: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 800px;
}
th,
td {
border: 1px solid var(--sl-color-neutral-200);
padding: 8px;
text-align: left;
}
th {
background-color: var(--sl-color-neutral-100);
}
.status-cell {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-indicator.running {
background-color: var(--sl-color-primary-600);
}
.status-indicator.pending {
background-color: var(--sl-color-warning-600);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: var(--sl-color-neutral-600);
}
.connection-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--sl-color-success-600);
}
.connection-dot.disconnected {
background-color: var(--sl-color-danger-600);
}
`,
];
@state()
private executions: FlowExecution[] = [];
@state()
private wsConnected = false;
@state()
private statusFilter = 'all';
@state()
private currentPage = 1;
@state()
private pageSize = 20;
private unsubscribe?: () => void;
async connectedCallback() {
super.connectedCallback();
await this.loadExecutions();
this.connectWebSocket();
}
async loadExecutions() {
const allExecutions = await getFlowExecutions();
// Sort by start_time descending (most recent first)
this.executions = allExecutions.sort(
(a, b) =>
parseUTCDate(b.start_time).getTime() -
parseUTCDate(a.start_time).getTime()
);
}
get filteredExecutions(): FlowExecution[] {
let filtered = this.executions;
// Apply status filter
if (this.statusFilter !== 'all') {
filtered = filtered.filter((exec) => exec.status === this.statusFilter);
}
return filtered;
}
get paginatedExecutions(): FlowExecution[] {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.filteredExecutions.slice(start, end);
}
get totalPages(): number {
return Math.ceil(this.filteredExecutions.length / this.pageSize);
}
handleStatusFilterChange(event: Event) {
const select = event.target as any;
this.statusFilter = select.value;
this.currentPage = 1; // Reset to first page when filter changes
}
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
}
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
}
connectWebSocket() {
// Subscribe to flow execution updates through unified WebSocket
this.unsubscribe = unifiedWebSocketManager.subscribe(
'flow_executions',
(message: any) => this.handleWebSocketMessage(message)
);
// Track connection state
unifiedWebSocketManager.onStateChange((state) => {
this.wsConnected = state === 'connected';
console.log(`Flow executions WebSocket state: ${state}`);
});
}
handleWebSocketMessage(message: any) {
console.log('Flow updates message:', message);
// Handle status updates
if (message.type === 'status_update' && message.execution_id) {
const executionIndex = this.executions.findIndex(
(exec) => exec.id === message.execution_id
);
if (executionIndex >= 0) {
// Update existing execution
const updated = [...this.executions];
updated[executionIndex] = {
...updated[executionIndex],
status: message.payload.status,
...(message.payload.end_time && {
end_time: message.payload.end_time,
}),
};
// Maintain sort order after update
this.executions = updated.sort(
(a, b) =>
parseUTCDate(b.start_time).getTime() -
parseUTCDate(a.start_time).getTime()
);
} else {
// New execution started, reload the list
this.loadExecutions();
}
}
// Handle new executions
if (message.type === 'execution_started' && message.payload) {
this.loadExecutions();
}
}
disconnectedCallback() {
super.disconnectedCallback();
// Unsubscribe from flow execution updates
this.unsubscribe?.();
}
render() {
return html`
<view-header headerText="Flow Executions" width="wide"></view-header>
<div class="column-layout wide">
<div class="main-column">
<div class="header-controls">
<div style="display: flex; gap: 12px; align-items: center;">
<sl-select
size="small"
value=${this.statusFilter}
@sl-change=${this.handleStatusFilterChange}
style="width: 150px;"
>
<sl-option value="all">All Status</sl-option>
<sl-option value="PENDING">Pending</sl-option>
<sl-option value="RUNNING">Running</sl-option>
<sl-option value="SUCCEEDED">Succeeded</sl-option>
<sl-option value="FAILED">Failed</sl-option>
<sl-option value="CANCELLED">Cancelled</sl-option>
</sl-select>
<sl-button size="small" @click=${this.loadExecutions}>
<sl-icon name="arrow-clockwise"></sl-icon>
Refresh
</sl-button>
</div>
<div class="connection-status">
<div
class="connection-dot ${this.wsConnected ? '' : 'disconnected'}"
></div>
<span>${this.wsConnected ? 'Live Updates' : 'Disconnected'}</span>
</div>
</div>
${this.paginatedExecutions.length === 0
? html`
<div
style="text-align: center; padding: 40px; color: var(--sl-color-neutral-600);"
>
<sl-icon name="inbox" style="font-size: 3rem;"></sl-icon>
<p>No executions found.</p>
</div>
`
: html`
<div
style="margin-bottom: 12px; color: var(--sl-color-neutral-600); font-size: 0.9rem;"
>
Showing ${(this.currentPage - 1) * this.pageSize + 1} -
${Math.min(
this.currentPage * this.pageSize,
this.filteredExecutions.length
)}
of ${this.filteredExecutions.length} executions
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Flow Name</th>
<th>Execution ID</th>
<th>Status</th>
<th>Start Time</th>
<th>End Time</th>
<th>Actions</th>
<th>Details</th>
</tr>
</thead>
<tbody>
${this.paginatedExecutions.map(
(exec) => html`
<tr>
<td>${exec.flow_name || 'Unnamed Flow'}</td>
<td>${exec.id.slice(0, 8)}...</td>
<td>
<div class="status-cell">
${exec.status === 'RUNNING' ||
exec.status === 'PENDING'
? html`
<div
class="status-indicator ${exec.status.toLowerCase()}"
></div>
`
: ''}
<sl-badge
variant=${this.getStatusVariant(exec.status)}
>${exec.status}</sl-badge
>
</div>
</td>
<td>${formatLocalDateTime(exec.start_time)}</td>
<td>
${exec.end_time
? formatLocalDateTime(exec.end_time)
: '-'}
</td>
<td>${exec.actions_taken_summary?.length || 0}</td>
<td>
<sl-button
size="small"
href=${router.urlForPath(
`/console/flows/executions/${exec.id}`
)}
>
<sl-icon name="eye"></sl-icon>
View
</sl-button>
</td>
</tr>
`
)}
</tbody>
</table>
</div>
${this.totalPages > 1
? html`
<div
style="display: flex; justify-content: space-between; align-items: center; margin-top: 16px; padding: 12px; background: var(--sl-color-neutral-50); border-radius: 4px;"
>
<sl-button
size="small"
@click=${this.prevPage}
?disabled=${this.currentPage === 1}
>
<sl-icon name="chevron-left"></sl-icon>
Previous
</sl-button>
<div style="color: var(--sl-color-neutral-700);">
Page ${this.currentPage} of ${this.totalPages}
</div>
<sl-button
size="small"
@click=${this.nextPage}
?disabled=${this.currentPage === this.totalPages}
>
Next
<sl-icon name="chevron-right"></sl-icon>
</sl-button>
</div>
`
: ''}
`}
</div>
</div>
`;
}
getStatusVariant(status: string) {
switch (status) {
case 'SUCCEEDED':
return 'success';
case 'FAILED':
return 'danger';
case 'RUNNING':
return 'primary';
default:
return 'neutral';
}
}
}