u
This commit is contained in:
327
src/examples/components/AsyncApiPrimaryView.jsx
Normal file
327
src/examples/components/AsyncApiPrimaryView.jsx
Normal file
@@ -0,0 +1,327 @@
|
||||
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',
|
||||
}}
|
||||
>
|
||||
“{data.statusMessage}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user