Files
automat/src/examples/components/AsyncApiPrimaryView.jsx
sebseb7 0f9321ac34 u
2026-09-22 11:38:20 +02:00

328 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PureComponent } from 'react';
import { profileAsyncAutomat } from '../automats/profileAsyncAutomat.js';
/**
* AsyncApiPrimaryView — Left column of Section 06.
*
* Demonstrates:
* 1. Initial access to an asynchronous Automat: constructor calls `profileAsyncAutomat.getData()`.
* 2. On first mount when data is not yet loaded, status is 'pending'.
* 3. The component renders `pending` (Render #1).
* 4. In `componentDidMount()`, it subscribes to updates.
* 5. When the simulated API fetch completes, `setState` runs and the component renders again (Render #2).
*/
export default class AsyncApiPrimaryView extends PureComponent {
_renderCount = 0;
_constructorStatus = 'uninitialized';
constructor(props) {
super(props);
// 1. Initial read in constructor triggers the async loader if idle:
this.state = profileAsyncAutomat.getData();
this._constructorStatus = this.state.status;
}
componentDidMount() {
// 2. Subscribe to receive update when API fetch completes:
this.unsubscribe = profileAsyncAutomat.subscribe(this);
}
componentWillUnmount() {
if (this.unsubscribe) this.unsubscribe();
}
handleReload = () => {
profileAsyncAutomat.reload();
};
handleReset = () => {
profileAsyncAutomat.actions.reset();
};
handleSimulateError = () => {
profileAsyncAutomat.actions.triggerError();
};
handleDelayChange = (ms) => {
profileAsyncAutomat.actions.setDelay(ms);
};
render() {
this._renderCount++;
const { status, data, error, delayMs, fetchCount, lastFetchedAt } = this.state;
return (
<div className="card">
<div className="card-header">
<span className="card-icon">⚡</span>
<h3>Primary API Consumer</h3>
<span className="badge badge-api">Constructor getData()</span>
</div>
<div className="card-body">
{/* Diagnostic render & constructor status bar */}
<div className="hybrid-state-bar" style={{ marginBottom: 14 }}>
<div className="state-item">
<div className="state-item-header">
<span className="state-tag-automat">Mount Renders</span>
<span>Render Count</span>
</div>
<span className="state-item-val" style={{ color: this._renderCount === 2 ? 'var(--green)' : 'var(--accent)' }}>
#{this._renderCount}
<span style={{ fontSize: 11, fontWeight: 400, marginLeft: 6, color: 'var(--text-3)' }}>
({this._renderCount === 1 ? '1st: Pending' : '2nd: Completed'})
</span>
</span>
</div>
<div className="state-item">
<div className="state-item-header">
<span className="state-tag-local">Constructor Read</span>
<span>Initial Status</span>
</div>
<span className="state-item-val" style={{ color: this._constructorStatus === 'pending' ? 'var(--orange)' : 'var(--green)' }}>
{this._constructorStatus}
</span>
</div>
</div>
{/* Main Visual Display Area */}
<div
style={{
background: 'var(--surface-0)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
padding: '20px',
minHeight: 200,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginBottom: 16,
}}
>
{/* Status: IDLE */}
{status === 'idle' && (
<div style={{ textAlign: 'center', padding: '24px 0' }}>
<span style={{ fontSize: 32, display: 'block', marginBottom: 8 }}>💤</span>
<div style={{ fontWeight: 600, color: 'var(--text-1)', marginBottom: 4 }}>
Cache Cleared (State: Idle)
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 14 }}>
Calling <code className="inline-code">getData()</code> will launch the delayed API request.
</div>
<button
type="button"
className="btn btn-primary"
onClick={this.handleReload}
>
Fetch Profile Now ({delayMs}ms delay)
</button>
</div>
)}
{/* Status: PENDING */}
{status === 'pending' && (
<div style={{ padding: '10px 0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<div className="spinner-gruvbox" />
<div>
<div style={{ fontWeight: 600, color: 'var(--orange)', fontSize: 14 }}>
Simulating API fetch... ({delayMs}ms delay)
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)' }}>
Constructor received <code className="inline-code">status: 'pending'</code>. Rendering 1st pass.
</div>
</div>
</div>
{/* Animated Gruvbox skeleton loaders */}
<div className="skeleton-bar" style={{ width: '45%', height: 16, marginBottom: 10 }} />
<div className="skeleton-bar" style={{ width: '85%', height: 12, marginBottom: 8 }} />
<div className="skeleton-bar" style={{ width: '65%', height: 12, marginBottom: 14 }} />
<div style={{ display: 'flex', gap: 8 }}>
<div className="skeleton-bar" style={{ width: 60, height: 20, borderRadius: 'var(--r-sm)' }} />
<div className="skeleton-bar" style={{ width: 80, height: 20, borderRadius: 'var(--r-sm)' }} />
<div className="skeleton-bar" style={{ width: 70, height: 20, borderRadius: 'var(--r-sm)' }} />
</div>
</div>
)}
{/* Status: ERROR */}
{status === 'error' && (
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<span style={{ fontSize: 32, display: 'block', marginBottom: 8 }}>⚠️</span>
<div style={{ fontWeight: 600, color: 'var(--red)', marginBottom: 6 }}>
API Fetch Failed
</div>
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 14 }}>
{error}
</div>
<button
type="button"
className="btn btn-secondary"
onClick={this.handleReload}
>
Retry Fetch
</button>
</div>
)}
{/* Status: SUCCESS */}
{status === 'success' && data && (
<div>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 34, lineHeight: 1 }}>{data.avatar}</span>
<div>
<div style={{ fontWeight: 700, fontSize: 16, color: 'var(--text-bright)' }}>
{data.name}
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)' }}>
{data.handle} · <span style={{ color: 'var(--aqua)' }}>{data.role}</span>
</div>
</div>
</div>
<span className="badge badge-success" style={{ fontSize: 10 }}>
200 OK
</span>
</div>
<div
style={{
background: 'var(--surface-1)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-md)',
padding: '10px 12px',
fontSize: 12,
color: 'var(--text-2)',
marginBottom: 12,
fontStyle: 'italic',
}}
>
&ldquo;{data.statusMessage}&rdquo;
</div>
{/* Quota bar & latency ping */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 12 }}>
<div style={{ background: 'var(--surface-1)', padding: '8px 10px', borderRadius: 'var(--r-sm)' }}>
<div style={{ fontSize: 11, color: 'var(--text-3)', marginBottom: 3 }}>
Cloud Quota ({data.quota.used}/{data.quota.total} {data.quota.unit})
</div>
<div style={{ height: 6, background: 'var(--surface-2)', borderRadius: 3, overflow: 'hidden' }}>
<div
style={{
height: '100%',
width: `${(data.quota.used / data.quota.total) * 100}%`,
background: 'var(--accent)',
borderRadius: 3,
}}
/>
</div>
</div>
<div style={{ background: 'var(--surface-1)', padding: '8px 10px', borderRadius: 'var(--r-sm)' }}>
<div style={{ fontSize: 11, color: 'var(--text-3)' }}>Cluster Latency</div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--green)' }}>
⚡ {data.pingMs}ms <span style={{ fontSize: 11, fontWeight: 400, color: 'var(--text-3)' }}>to internal VPC</span>
</div>
</div>
</div>
{/* Tags */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{data.tags.map((tag) => (
<span
key={tag}
style={{
fontSize: 10,
padding: '2px 7px',
background: 'var(--surface-2)',
borderRadius: 'var(--r-sm)',
color: 'var(--text-2)',
border: '1px solid var(--border)',
}}
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
{/* Latency Simulator & Control Actions */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 6, display: 'flex', justifyContent: 'space-between' }}>
<span>Simulated Network Latency:</span>
<strong style={{ color: 'var(--accent)' }}>{delayMs}ms</strong>
</div>
<div className="segmented-control" style={{ width: '100%', display: 'flex' }}>
<button
type="button"
className={`seg-btn ${delayMs === 300 ? 'active' : ''}`}
style={{ flex: 1 }}
onClick={() => this.handleDelayChange(300)}
>
Fast (300ms)
</button>
<button
type="button"
className={`seg-btn ${delayMs === 800 ? 'active' : ''}`}
style={{ flex: 1 }}
onClick={() => this.handleDelayChange(800)}
>
Normal (800ms)
</button>
<button
type="button"
className={`seg-btn ${delayMs === 1800 ? 'active' : ''}`}
style={{ flex: 1 }}
onClick={() => this.handleDelayChange(1800)}
>
Slow (1.8s)
</button>
</div>
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
<button
id="btn-primary-refetch"
type="button"
className="btn btn-primary"
style={{ flex: 1 }}
onClick={this.handleReload}
>
🔄 Force Reload
</button>
<button
id="btn-primary-reset"
type="button"
className="btn btn-secondary"
style={{ flex: 1 }}
onClick={this.handleReset}
>
🗑️ Clear Cache (Idle)
</button>
<button
id="btn-primary-error"
type="button"
className="btn btn-secondary"
title="Simulate 504 Timeout"
onClick={this.handleSimulateError}
>
💥 Timeout
</button>
</div>
{/* Footer Metadata */}
<div style={{ fontSize: 11, color: 'var(--text-3)', display: 'flex', justifyContent: 'space-between' }}>
<span>API Fetch Calls: <strong style={{ color: 'var(--text-1)' }}>{fetchCount}</strong></span>
<span>Last Sync: <strong style={{ color: 'var(--text-1)' }}>{lastFetchedAt || 'Never'}</strong></span>
</div>
</div>
</div>
);
}
}