1+ @inherits Apollo .Components .DynamicTabs .DynamicTabView
2+ @using Apollo .Components .DynamicTabs
3+ @using Apollo .Components .DynamicTabs .Commands
4+ @using Apollo .Components .DynamicClient .Commands
5+ @using Apollo .Components .Hosting
6+ @using Apollo .Components .Hosting .Commands
7+ @using Apollo .Components .Infrastructure .MessageBus
8+ @using Apollo .Components .Shared
9+ @using Apollo .Components .Solutions
10+ @using Apollo .Components .Solutions .Commands
11+ @using Apollo .Components .Theme
12+ @using Apollo .Contracts .Solutions
13+ @using Microsoft .JSInterop
14+ @implements IAsyncDisposable
15+
16+ <div class =" d-flex flex-column mud-height-full" style =" background : var (--mud-palette-background );" >
17+ <div class =" client-toolbar d-flex align-center gap-2 pa-2"
18+ style =" background : var (--mud-palette-surface ); border-bottom : 1px solid var (--mud-palette-lines-default );" >
19+ <ApolloIconButton Icon =" @(_isLoading? Icons.Material.Filled.HourglassEmpty : Icons.Material.Filled.Refresh)"
20+ Tooltip =" Refresh" Size =" Size.Small" Disabled =" @_isLoading" OnClick =" RefreshPreview" />
21+
22+ <div class =" url-bar flex-grow-1 d-flex align-center px-2 py-1 rounded"
23+ style =" background : var (--mud-palette-background ); border : 1px solid var (--mud-palette-lines-inputs );" >
24+ <MudIcon Icon =" @Icons.Material.Filled.Lock" Size =" Size.Small" Class =" mr-2"
25+ Style =" color: var(--mud-palette-success);" />
26+ <MudText Typo =" Typo.body2" Class =" flex-grow-1" >localhost (virtual)</MudText >
27+ </div >
28+
29+ <MudTooltip Text =" Open in floating window" >
30+ <ApolloIconButton Icon =" @Icons.Material.Filled.OpenInNew" Size =" Size.Small"
31+ OnClick =" @(async () => await Bus.PublishAsync(new UpdateTabLocationByName(Name, DropZones.Floating)))" />
32+ </MudTooltip >
33+ </div >
34+
35+ <div class =" preview-container flex-grow-1 position-relative" style =" overflow : hidden ;" >
36+ @if (! HostingService .Hosting )
37+ {
38+ <div class =" d-flex flex-column align-center justify-center mud-height-full gap-4 pa-4" >
39+ <MudIcon Icon =" @Icons.Material.Filled.CloudOff" Size =" Size.Large"
40+ Style =" color: var(--mud-palette-text-secondary);" />
41+ <MudText Typo =" Typo.h6" >API Server Not Running </MudText >
42+ <MudText Typo =" Typo.body2" Class =" mud-text-secondary text-center" Style =" max-width: 300px;" >
43+ Start your Web API project first , then the client preview will connect automatically .
44+ </MudText >
45+ <MudButton Variant =" Variant.Filled" Color =" Color.Success" StartIcon =" @ApolloIcons.Run"
46+ Disabled =" @(State.Project?.ProjectType != ProjectType.WebApi)" OnClick =" StartApiAndClient" >
47+ Run Full - Stack
48+ </MudButton >
49+ </div >
50+ }
51+ else if (string .IsNullOrEmpty (_documentContent ))
52+ {
53+ <div class =" d-flex flex-column align-center justify-center mud-height-full gap-4 pa-4" >
54+ <MudIcon Icon =" @Icons.Material.Filled.WebAsset" Size =" Size.Large"
55+ Style =" color: var(--mud-palette-text-secondary);" />
56+ <MudText Typo =" Typo.h6" >No Client Found </MudText >
57+ <MudText Typo =" Typo.body2" Class =" mud-text-secondary text-center" Style =" max-width: 300px;" >
58+ Add an <code >index .html </code > file to your solution to enable client preview .
59+ </MudText >
60+ </div >
61+ }
62+ else
63+ {
64+ <iframe @ref =" _iframeRef" class =" client-iframe" sandbox =" allow-scripts allow-forms allow-modals"
65+ style =" width : 100% ; height : 100% ; border : none ; background : white ;" >
66+ </iframe >
67+ }
68+ </div >
69+
70+ @if (_requestCount > 0 )
71+ {
72+ <div class =" status-bar d-flex align-center justify-space-between px-2 py-1"
73+ style =" background : var (--mud-palette-surface ); border-top : 1px solid var (--mud-palette-lines-default ); font-size : 0.75rem ;" >
74+ <MudText Typo =" Typo.caption" >@_requestCount requests </MudText >
75+ <MudLink Typo =" Typo.caption" OnClick =" @(() => Bus.PublishAsync(new FocusTab(" Network " )))" >
76+ View Network Log
77+ </MudLink >
78+ </div >
79+ }
80+ </div >
81+
82+ @code {
83+ [Inject ] private IDynamicClientService ClientService { get ; set ; } = default ! ;
84+ [Inject ] private IHostingService HostingService { get ; set ; } = default ! ;
85+ [Inject ] private SolutionsState State { get ; set ; } = default ! ;
86+ [Inject ] private IMessageBus Bus { get ; set ; } = default ! ;
87+ [Inject ] private IJSRuntime JsRuntime { get ; set ; } = default ! ;
88+
89+ private ElementReference _iframeRef ;
90+ private DotNetObjectReference <ClientPreviewTab >? _dotNetRef ;
91+ private string ? _documentContent ;
92+ private bool _isLoading ;
93+ private int _requestCount ;
94+ private bool _jsInitialized ;
95+
96+ public override string Name { get ; set ; } = " Client Preview" ;
97+ public override Type ComponentType { get ; set ; } = typeof (ClientPreviewTab );
98+ public override string DefaultArea => DropZones .None ;
99+
100+ protected override async Task OnInitializedAsync ()
101+ {
102+ await base .OnInitializedAsync ();
103+
104+ HostingService .OnHostingStateChanged += HandleHostingStateChanged ;
105+ HostingService .OnRoutesChanged += HandleRoutesChanged ;
106+ ClientService .OnRequestLogged += HandleRequestLogged ;
107+ State .SolutionFilesChanged += HandleFilesChanged ;
108+
109+ if (HostingService .Hosting && HostingService .Routes ? .Count > 0 )
110+ {
111+ await LoadClientDocument ();
112+ }
113+ }
114+
115+ private async Task HandleRoutesChanged ()
116+ {
117+ if (HostingService .Routes ? .Count > 0 )
118+ {
119+ await LoadClientDocument ();
120+ }
121+ await InvokeAsync (StateHasChanged );
122+ }
123+
124+ protected override async Task OnAfterRenderAsync (bool firstRender )
125+ {
126+ if (firstRender )
127+ {
128+ _dotNetRef = DotNetObjectReference .Create (this );
129+ }
130+
131+ if (! string .IsNullOrEmpty (_documentContent ) && ! _jsInitialized )
132+ {
133+ await InitializeIframe ();
134+ }
135+ }
136+
137+ private async Task InitializeIframe ()
138+ {
139+ if (_dotNetRef == null ) return ;
140+
141+ try
142+ {
143+ await JsRuntime .InvokeVoidAsync (" apolloClientPreview.initialize" , _iframeRef , _dotNetRef , _documentContent );
144+ _jsInitialized = true ;
145+ }
146+ catch (Exception ex )
147+ {
148+ Console .WriteLine ($" Failed to initialize iframe: {ex .Message }" );
149+ }
150+ }
151+
152+ [JSInvokable ]
153+ public async Task <object > HandleApiRequest (int id , string method , string url , string ? body )
154+ {
155+ var response = await ClientService .HandleRequestAsync (method , url , body );
156+ _requestCount ++ ;
157+ await InvokeAsync (StateHasChanged );
158+
159+ return new
160+ {
161+ id ,
162+ status = response .StatusCode ,
163+ body = response .Body ,
164+ headers = response .Headers ?? new Dictionary <string , string > { { " Content-Type" , " application/json" } }
165+ };
166+ }
167+
168+ private async Task HandleHostingStateChanged ()
169+ {
170+ if (! HostingService .Hosting )
171+ {
172+ _documentContent = null ;
173+ _jsInitialized = false ;
174+ }
175+
176+ await InvokeAsync (StateHasChanged );
177+ }
178+
179+ private void HandleRequestLogged (NetworkRequest request )
180+ {
181+ _requestCount = ClientService .RequestLog .Count ;
182+ InvokeAsync (StateHasChanged );
183+ }
184+
185+ private void HandleFilesChanged ()
186+ {
187+ if (HostingService .Hosting && State .Project != null )
188+ {
189+ _ = RefreshPreview ();
190+ }
191+ }
192+
193+ private async Task LoadClientDocument ()
194+ {
195+ if (State .Project == null ) return ;
196+
197+ _documentContent = ClientService .BuildClientDocument (State .Project );
198+ _jsInitialized = false ;
199+ await InvokeAsync (StateHasChanged );
200+ }
201+
202+ private async Task RefreshPreview ()
203+ {
204+ _isLoading = true ;
205+ StateHasChanged ();
206+
207+ try
208+ {
209+ _jsInitialized = false ;
210+ await LoadClientDocument ();
211+
212+ if (! string .IsNullOrEmpty (_documentContent ))
213+ {
214+ await Task .Delay (50 );
215+ await InitializeIframe ();
216+ }
217+ }
218+ finally
219+ {
220+ _isLoading = false ;
221+ StateHasChanged ();
222+ }
223+ }
224+
225+ private async Task StartApiAndClient ()
226+ {
227+ if (State .Project == null ) return ;
228+
229+ await Bus .PublishAsync (new StartRunning ());
230+ await ClientService .StartAsync (State .Project );
231+ }
232+
233+ public async ValueTask DisposeAsync ()
234+ {
235+ HostingService .OnHostingStateChanged -= HandleHostingStateChanged ;
236+ HostingService .OnRoutesChanged -= HandleRoutesChanged ;
237+ ClientService .OnRequestLogged -= HandleRequestLogged ;
238+ State .SolutionFilesChanged -= HandleFilesChanged ;
239+
240+ _dotNetRef ? .Dispose ();
241+
242+ try
243+ {
244+ await JsRuntime .InvokeVoidAsync (" apolloClientPreview.dispose" );
245+ }
246+ catch
247+ {
248+ }
249+ }
250+ }
0 commit comments