Skip to content

Commit 7b6c460

Browse files
authored
Merge pull request #5 from Navigraph/chore/ui-improvements
2 parents 2d8906f + 759fd04 commit 7b6c460

File tree

5 files changed

+235
-37
lines changed

5 files changed

+235
-37
lines changed

examples/gauge/Components/Input.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ComponentProps, DisplayComponent, FSComponent, Subscribable, UUID, VNode } from "@microsoft/msfs-sdk"
2+
3+
interface InputProps extends ComponentProps {
4+
value?: string
5+
class?: string | Subscribable<string>
6+
textarea?: boolean
7+
}
8+
9+
export class Input extends DisplayComponent<InputProps> {
10+
private readonly inputId = UUID.GenerateUuid()
11+
private readonly inputRef = FSComponent.createRef<HTMLInputElement>()
12+
13+
get value() {
14+
return this.inputRef.instance.value
15+
}
16+
17+
onAfterRender(node: VNode): void {
18+
super.onAfterRender(node)
19+
20+
this.inputRef.instance.onfocus = this.onInputFocus
21+
this.inputRef.instance.onblur = this.onInputBlur
22+
}
23+
24+
private getInputProps() {
25+
return { value: this.props.value, class: this.props.class }
26+
}
27+
28+
/**
29+
* Method to handle when input focus is set
30+
* @param e The focus event.
31+
*/
32+
private onInputFocus = (e: FocusEvent): void => {
33+
e.preventDefault()
34+
35+
Coherent.trigger("FOCUS_INPUT_FIELD", this.inputId, "", "", this.inputRef.instance.value, false)
36+
Coherent.on("mousePressOutsideView", () => this.inputRef.instance.blur())
37+
}
38+
39+
/**
40+
* Method to handle on input blur
41+
*/
42+
private onInputBlur = (): void => {
43+
Coherent.trigger("UNFOCUS_INPUT_FIELD", "")
44+
Coherent.off("mousePressOutsideView")
45+
}
46+
47+
render() {
48+
if (this.props.textarea)
49+
return (
50+
<textarea style="width:350px;height:100px;" ref={this.inputRef} {...this.getInputProps()}>
51+
{this.props.value}
52+
</textarea>
53+
)
54+
return <input ref={this.inputRef} {...this.getInputProps()} />
55+
}
56+
}

examples/gauge/Components/InterfaceSample.css

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
width: 100%;
44
height: 100%;
55
position: relative;
6-
top: 100px;
6+
}
7+
8+
.loading-container {
9+
display: flex;
10+
justify-content: center;
11+
align-items: center;
12+
width: 100%;
13+
height: 100%;
14+
font-size: x-large;
15+
text-align: center;
716
}
817

918
.button {
@@ -19,16 +28,21 @@
1928
}
2029

2130
.qr-code {
22-
width: 300px;
23-
height: 300px;
31+
margin-top: 10px;
32+
width: 250px;
33+
height: 250px;
2434
display: none;
35+
background: white;
36+
padding: 10px;
37+
border-radius: 5px;
2538
}
2639

2740
.horizontal {
2841
display: flex;
2942
flex-direction: row;
30-
justify-content: center;
31-
align-items: center;
43+
justify-content: space-evenly;
44+
align-items: flex-start;
45+
padding: 2rem;
3246
}
3347

3448
.vertical {
@@ -49,3 +63,16 @@
4963
justify-content: center;
5064
align-items: center;
5165
}
66+
67+
pre {
68+
background: #272727;
69+
border-radius: 6px;
70+
padding: 10px;
71+
72+
min-width: 300px;
73+
min-height: 200px;
74+
}
75+
76+
h4 {
77+
margin-bottom: 15px;
78+
}

examples/gauge/Components/InterfaceSample.tsx

Lines changed: 130 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import { ComponentProps, DisplayComponent, EventBus, FSComponent, VNode } from "@microsoft/msfs-sdk"
2-
import { CancelToken } from "navigraph/auth"
3-
import { packages } from "../Lib/navigraph"
4-
import { AuthService } from "../Services/AuthService"
5-
import "./InterfaceSample.css"
1+
import {
2+
ComponentProps,
3+
DisplayComponent,
4+
EventBus,
5+
FSComponent,
6+
MappedSubject,
7+
Subject,
8+
VNode,
9+
} from "@microsoft/msfs-sdk"
610
import {
711
DownloadProgressPhase,
812
NavigraphEventType,
913
NavigraphNavigationDataInterface,
1014
} from "@navigraph/msfs-navigation-data-interface"
15+
import { NavigationDataStatus } from "@navigraph/msfs-navigation-data-interface/types/meta"
16+
import { CancelToken } from "navigraph/auth"
17+
import { packages } from "../Lib/navigraph"
18+
import { AuthService } from "../Services/AuthService"
1119
import { Dropdown } from "./Dropdown"
20+
import { Input } from "./Input"
21+
import "./InterfaceSample.css"
1222

1323
interface InterfaceSampleProps extends ComponentProps {
1424
bus: EventBus
@@ -21,8 +31,15 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
2131
private readonly qrCodeRef = FSComponent.createRef<HTMLImageElement>()
2232
private readonly dropdownRef = FSComponent.createRef<Dropdown>()
2333
private readonly downloadButtonRef = FSComponent.createRef<HTMLButtonElement>()
24-
private readonly executeButtonRef = FSComponent.createRef<HTMLButtonElement>()
25-
private readonly inputRef = FSComponent.createRef<HTMLInputElement>()
34+
private readonly icaoInputRef = FSComponent.createRef<Input>()
35+
private readonly executeIcaoButtonRef = FSComponent.createRef<HTMLButtonElement>()
36+
private readonly sqlInputRef = FSComponent.createRef<Input>()
37+
private readonly executeSqlButtonRef = FSComponent.createRef<HTMLButtonElement>()
38+
private readonly outputRef = FSComponent.createRef<HTMLPreElement>()
39+
private readonly loadingRef = FSComponent.createRef<HTMLDivElement>()
40+
private readonly authContainerRef = FSComponent.createRef<HTMLDivElement>()
41+
42+
private readonly navigationDataStatus = Subject.create<NavigationDataStatus | null>(null)
2643

2744
private cancelSource = CancelToken.source()
2845

@@ -53,50 +70,123 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
5370
})
5471
}
5572

56-
public render(): VNode {
73+
public renderDatabaseStatus(): VNode | void {
5774
return (
58-
<div class="auth-container">
59-
<div class="horizontal">
60-
<div class="vertical">
61-
<div ref={this.textRef}>Loading</div>
62-
<div ref={this.loginButtonRef} class="button" />
63-
<div ref={this.navigationDataTextRef} />
64-
<img ref={this.qrCodeRef} class="qr-code" />
75+
<>
76+
<div
77+
class={MappedSubject.create(([status]) => {
78+
return status ? "vertical" : "hidden"
79+
}, this.navigationDataStatus)}
80+
>
81+
<div>{this.navigationDataStatus.map(s => `Install method: ${s?.status}`)}</div>
82+
<div>
83+
{this.navigationDataStatus.map(
84+
s => `Installed format: ${s?.installedFormat} revision ${s?.installedRevision}`,
85+
)}
6586
</div>
66-
<div class="vertical">
67-
<Dropdown ref={this.dropdownRef} />
68-
<div ref={this.downloadButtonRef} class="button">
69-
Download
87+
<div>{this.navigationDataStatus.map(s => `Installed path: ${s?.installedPath}`)}</div>
88+
<div>{this.navigationDataStatus.map(s => `Installed cycle: ${s?.installedCycle}`)}</div>
89+
<div>{this.navigationDataStatus.map(s => `Latest cycle: ${s?.latestCycle}`)}</div>
90+
<div>{this.navigationDataStatus.map(s => `Validity period: ${s?.validityPeriod}`)}</div>
91+
</div>
92+
<div class={this.navigationDataStatus.map(status => (status ? "hidden" : "visible"))}>Loading status...</div>
93+
</>
94+
)
95+
}
96+
97+
public render(): VNode {
98+
return (
99+
<>
100+
<div class="loading-container" ref={this.loadingRef}>
101+
Waiting for navigation data interface to initialize... If building for the first time, this may take a few
102+
minutes
103+
</div>
104+
<div class="auth-container" ref={this.authContainerRef} style={{ display: "none" }}>
105+
<div class="horizontal">
106+
<div class="vertical">
107+
<h4>Step 1 - Sign in</h4>
108+
<div ref={this.textRef}>Loading</div>
109+
<div ref={this.loginButtonRef} class="button" />
110+
<div ref={this.navigationDataTextRef} />
111+
<img ref={this.qrCodeRef} class="qr-code" />
112+
</div>
113+
<div class="vertical">
114+
<h4>Step 2 - Select Database</h4>
115+
<Dropdown ref={this.dropdownRef} />
116+
<div ref={this.downloadButtonRef} class="button">
117+
Download
118+
</div>
119+
{this.renderDatabaseStatus()}
70120
</div>
71-
<input ref={this.inputRef} type="text" id="sql" name="sql" value="ESSA" class="text-field" />
72-
<div ref={this.executeButtonRef} class="button">
73-
Execute SQL
121+
</div>
122+
123+
<h4 style="text-align: center;">Step 3 - Query the database</h4>
124+
<div class="horizontal">
125+
<div class="vertical">
126+
<Input ref={this.icaoInputRef} value="TNCM" class="text-field" />
127+
<div ref={this.executeIcaoButtonRef} class="button">
128+
Fetch Airport
129+
</div>
130+
<div style="height:30px;"></div>
131+
<Input
132+
ref={this.sqlInputRef}
133+
textarea
134+
value="SELECT airport_name FROM tbl_airports WHERE airport_identifier = 'TNCM'"
135+
class="text-field"
136+
/>
137+
<div ref={this.executeSqlButtonRef} class="button">
138+
Execute SQL
139+
</div>
74140
</div>
141+
<pre ref={this.outputRef} id="output">
142+
The output of the query will show up here
143+
</pre>
75144
</div>
76145
</div>
77-
</div>
146+
</>
78147
)
79148
}
80149

81-
public onBeforeRender(): void {
82-
super.onBeforeRender()
83-
}
84-
85150
public onAfterRender(node: VNode): void {
86151
super.onAfterRender(node)
87152

153+
// Populate status when ready
154+
this.navigationDataInterface.onReady(() => {
155+
this.navigationDataInterface
156+
.get_navigation_data_install_status()
157+
.then(status => this.navigationDataStatus.set(status))
158+
.catch(e => console.error(e))
159+
160+
// show the auth container
161+
this.authContainerRef.instance.style.display = "block"
162+
this.loadingRef.instance.style.display = "none"
163+
})
164+
88165
this.loginButtonRef.instance.addEventListener("click", () => this.handleClick())
89166
this.downloadButtonRef.instance.addEventListener("click", () => this.handleDownloadClick())
90167

91-
this.executeButtonRef.instance.addEventListener("click", () => {
168+
this.executeIcaoButtonRef.instance.addEventListener("click", () => {
92169
console.time("query")
93170
this.navigationDataInterface
94-
.get_airport(this.inputRef.instance.value)
171+
.get_airport(this.icaoInputRef.instance.value)
95172
.then(airport => {
96173
console.info(airport)
97-
console.timeEnd("query")
174+
this.outputRef.instance.textContent = JSON.stringify(airport, null, 2)
98175
})
99176
.catch(e => console.error(e))
177+
.finally(() => console.timeEnd("query"))
178+
})
179+
180+
this.executeSqlButtonRef.instance.addEventListener("click", () => {
181+
console.time("query")
182+
this.navigationDataInterface
183+
.execute_sql(this.sqlInputRef.instance.value, [])
184+
.then(result => {
185+
console.info(result)
186+
this.outputRef.instance.textContent = JSON.stringify(result, null, 2)
187+
})
188+
.catch(e => console.error(e))
189+
.finally(() => console.timeEnd("query"))
100190
})
101191

102192
AuthService.user.sub(user => {
@@ -105,6 +195,7 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
105195
this.qrCodeRef.instance.style.display = "none"
106196
this.loginButtonRef.instance.textContent = "Log out"
107197
this.textRef.instance.textContent = `Welcome, ${user.preferred_username}`
198+
this.displayMessage("")
108199

109200
this.handleLogin()
110201
} else {
@@ -120,6 +211,7 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
120211
await AuthService.signOut()
121212
} else {
122213
this.cancelSource = CancelToken.source() // Reset any previous cancellations
214+
this.displayMessage("Authenticating.. Scan code (or click it) to sign in")
123215
await AuthService.signIn(p => {
124216
if (p) {
125217
this.qrCodeRef.instance.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${p.verification_uri_complete}`
@@ -131,6 +223,7 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
131223
}, this.cancelSource.token)
132224
}
133225
} catch (err) {
226+
this.qrCodeRef.instance.style.display = "none"
134227
if (err instanceof Error) this.displayError(err.message)
135228
else this.displayError(`Unknown error: ${String(err)}`)
136229
}
@@ -160,6 +253,13 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
160253

161254
// Download navigation data to work dir
162255
await this.navigationDataInterface.download_navigation_data(pkg.file.url)
256+
257+
// Update navigation data status
258+
this.navigationDataInterface
259+
.get_navigation_data_install_status()
260+
.then(status => this.navigationDataStatus.set(status))
261+
.catch(e => console.error(e))
262+
163263
this.displayMessage("Navigation data downloaded")
164264
} catch (err) {
165265
if (err instanceof Error) this.displayError(err.message)

examples/gauge/global.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface CoherentEngine {
2+
/**
3+
* Asynchronously call a C++ handler and retrieve the result
4+
* @param name name of the C++ handler to be called
5+
* @param args any extra parameters to be passed to the C++ handler
6+
* @return promise for the result of the C++ function
7+
*/
8+
call(name: "PLAY_INSTRUMENT_SOUND", soundName: string): Promise<void>
9+
call(name: string, ...args: unknown[]): Promise<unknown>
10+
11+
on(name: "SetInputTextFromOS" | "mousePressOutsideView", cb: () => void): void
12+
off(name: "SetInputTextFromOS" | "mousePressOutsideView", cb?: () => void): void
13+
14+
trigger(name: "FOCUS_INPUT_FIELD" | "UNFOCUS_INPUT_FIELD", ...args: unknown[])
15+
}

src/js/types/meta.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export enum InstallStatus {
77
export interface NavigationDataStatus {
88
status: InstallStatus
99
installedFormat: string | null
10-
installedRegion: string | null
10+
installedRevision: string | null
1111
installedCycle: string | null
1212
installedPath: string | null
1313
validityPeriod: string | null
14-
lastestCycle: string | null
14+
latestCycle: string | null
1515
}

0 commit comments

Comments
 (0)