Skip to content

Commit 0aede90

Browse files
committed
Implemented emulator overlay
1 parent 8f6167e commit 0aede90

File tree

5 files changed

+97
-127
lines changed

5 files changed

+97
-127
lines changed

common/api-review/util.api.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,8 @@ export interface Subscribe<T> {
490490
// @public (undocumented)
491491
export type Unsubscribe = () => void;
492492

493-
// Warning: (ae-missing-release-tag) "updateStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
494-
//
495-
// @public (undocumented)
496-
export function updateStatus(name: string, isRunningEmulator: boolean): void;
493+
// @public
494+
export function updateEmulatorBanner(name: string, isRunningEmulator: boolean): void;
497495

498496
// Warning: (ae-missing-release-tag) "validateArgCount" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
499497
//

packages/auth/src/api/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
isCloudflareWorker,
2121
isCloudWorkstation,
2222
querystring,
23-
updateStatus
23+
updateEmulatorBanner
2424
} from '@firebase/util';
2525

2626
import { AuthErrorCode, NamedErrorParams } from '../core/errors';
@@ -200,10 +200,7 @@ export async function _performFetchWithErrorHandling<V>(
200200
fetchFn: () => Promise<Response>
201201
): Promise<V> {
202202
const authInternal = auth as AuthInternal;
203-
updateStatus(
204-
'Auth',
205-
authInternal.emulatorConfig !== null
206-
);
203+
updateEmulatorBanner('Auth', authInternal.emulatorConfig !== null);
207204
authInternal._canInitEmulator = false;
208205
const errorMap = { ...SERVER_ERROR_MAP, ...customErrorMap };
209206
try {

packages/auth/src/core/auth/emulator.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
deepEqual,
2323
isCloudWorkstation,
2424
pingServer,
25-
updateStatus
25+
updateEmulatorBanner
2626
} from '@firebase/util';
2727

2828
/**
@@ -102,13 +102,14 @@ export function connectAuthEmulator(
102102
authInternal.emulatorConfig = emulatorConfig;
103103
authInternal.settings.appVerificationDisabledForTesting = true;
104104

105-
if (!disableWarnings) {
106-
emitEmulatorWarning(isCloudWorkstation(emulatorConfig.host));
105+
if (!disableWarnings && !isCloudWorkstation(host)) {
106+
emitEmulatorWarning();
107107
}
108108

109109
// Workaround to get cookies in Firebase Studio
110110
if (isCloudWorkstation(host)) {
111111
void pingServer(`${protocol}//${host}:${port}`);
112+
updateEmulatorBanner('Auth', true);
112113
}
113114
}
114115

@@ -148,16 +149,38 @@ function parsePort(portStr: string): number | null {
148149
return port;
149150
}
150151

151-
function emitEmulatorWarning(isCloudWorkstation: boolean): void {
152-
updateStatus(
153-
'Auth',
154-
true
155-
);
152+
function emitEmulatorWarning(): void {
153+
function attachBanner(): void {
154+
const el = document.createElement('p');
155+
const sty = el.style;
156+
el.innerText =
157+
'Running in emulator mode. Do not use with production credentials.';
158+
sty.position = 'fixed';
159+
sty.width = '100%';
160+
sty.backgroundColor = '#ffffff';
161+
sty.border = '.1em solid #000000';
162+
sty.color = '#b50000';
163+
sty.bottom = '0px';
164+
sty.left = '0px';
165+
sty.margin = '0px';
166+
sty.zIndex = '10000';
167+
sty.textAlign = 'center';
168+
el.classList.add('firebase-emulator-warning');
169+
document.body.appendChild(el);
170+
}
171+
156172
if (typeof console !== 'undefined' && typeof console.info === 'function') {
157173
console.info(
158174
'WARNING: You are using the Auth Emulator,' +
159175
' which is intended for local testing only. Do not use with' +
160176
' production credentials.'
161177
);
162178
}
179+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
180+
if (document.readyState === 'loading') {
181+
window.addEventListener('DOMContentLoaded', attachBanner);
182+
} else {
183+
attachBanner();
184+
}
185+
}
163186
}

packages/firestore/src/lite-api/database.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
EmulatorMockTokenOptions,
2929
getDefaultEmulatorHostnameAndPort,
3030
isCloudWorkstation,
31-
updateStatus,
31+
updateEmulatorBanner,
3232
pingServer
3333
} from '@firebase/util';
3434

@@ -143,9 +143,9 @@ export class Firestore implements FirestoreService {
143143

144144
_freezeSettings(): FirestoreSettingsImpl {
145145
this._settingsFrozen = true;
146-
updateStatus(
147-
'Firestore',
148-
(this._settings as PrivateSettings).emulator!!
146+
updateEmulatorBanner(
147+
'Firestore',
148+
(this._settings as PrivateSettings).emulator!!
149149
);
150150
return this._settings;
151151
}
@@ -339,9 +339,7 @@ export function connectFirestoreEmulator(
339339
emulatorOptions: firestore._getEmulatorOptions()
340340
};
341341
const newHostSetting = `${host}:${port}`;
342-
if (useSsl) {
343-
void pingServer(`https://${newHostSetting}`);
344-
}
342+
345343
if (settings.host !== DEFAULT_HOST && settings.host !== newHostSetting) {
346344
logWarn(
347345
'Host has been set in both settings() and connectFirestoreEmulator(), emulator host ' +
@@ -362,6 +360,11 @@ export function connectFirestoreEmulator(
362360

363361
firestore._setSettings(newConfig);
364362

363+
if (useSsl) {
364+
void pingServer(`https://${newHostSetting}`);
365+
updateEmulatorBanner('firestore', true);
366+
}
367+
365368
if (options.mockUserToken) {
366369
let token: string;
367370
let user: User;

packages/util/src/emulator.ts

Lines changed: 52 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,6 @@ export function createMockUserToken(
141141
].join('.');
142142
}
143143

144-
function getOrCreate(id: string): { created: boolean; element: HTMLElement } {
145-
let parentDiv = document.getElementById(id);
146-
let created = false;
147-
if (!parentDiv) {
148-
parentDiv = document.createElement('div');
149-
parentDiv.setAttribute('id', id);
150-
created = true;
151-
}
152-
return { created, element: parentDiv };
153-
}
154-
155144
interface EmulatorStatuses {
156145
[name: string]: boolean;
157146
}
@@ -161,108 +150,82 @@ export interface EmulatorStatus {
161150
isRunningEmulator: boolean;
162151
}
163152

164-
function createPopover() {
165-
const popover = document.createElement('div');
166-
popover.setAttribute('id', 'firebase__popover');
167-
popover.innerText = "I'm a popover!";
168-
popover.style.padding = '1em';
169-
popover.style.display = 'none';
170-
popover.style.position = 'absolute';
171-
popover.style.top = '50px';
172-
return popover;
153+
// Checks whether any products are running on an emulator
154+
function areRunningEmulator() {
155+
let runningEmulator = false;
156+
for (let key of Object.keys(emulatorStatus)) {
157+
if (emulatorStatus[key]) {
158+
runningEmulator = true;
159+
}
160+
}
161+
return runningEmulator;
173162
}
174163

175-
function createFirebaseEl() {
176-
const firebaseIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
177-
firebaseIcon.setAttribute('width', '25');
178-
firebaseIcon.setAttribute('height', '25');
179-
firebaseIcon.setAttribute('viewBox', '0 0 600 600');
180-
firebaseIcon.setAttribute('fill', 'none');
181-
182-
firebaseIcon.setAttribute('xlmns', 'http://www.w3.org/2000/svg');
183-
firebaseIcon.innerHTML =
184-
'<path d="M518.293 327.327V327.152C518.29 327.087 518.29 327.021 518.293 326.955C513.042 292.744 499.418 261.23 481.346 232.883C479.073 229.299 476.709 225.776 474.292 222.275C473.12 220.578 471.93 218.896 470.723 217.229C447.484 184.98 374.082 97.3028 335.522 51.5891C320.935 34.2901 311.335 23 311.335 23C306.986 26.4856 302.735 30.0822 298.582 33.79L298.438 33.9188C268.366 62.0273 243.429 95.1671 224.749 131.848L224.689 131.961C221.092 139.034 217.733 146.232 214.611 153.557C207.388 170.525 201.518 188.038 197.054 205.931C194.347 216.746 192.154 227.751 190.477 238.945C181.248 237.435 171.926 236.559 162.578 236.323C160.809 236.278 159.041 236.255 157.273 236.255C141.997 236.244 126.767 237.947 111.87 241.332C102.752 256.057 95.4109 271.812 90.0022 288.266C82.691 310.534 78.9771 333.826 79.0001 357.264C79.0001 464.392 155.334 553.706 256.581 573.747C270.78 576.56 285.221 577.974 299.696 577.968C302.553 577.968 305.394 577.907 308.228 577.809C315.669 577.516 323.006 576.869 330.24 575.869C347.097 573.532 363.625 569.245 379.493 563.094C383.721 561.457 387.883 559.689 391.98 557.79C467.753 522.851 520.392 446.199 520.392 357.264C520.363 347.249 519.662 337.248 518.293 327.327ZM228.462 525.692C206.732 516.512 186.992 503.2 170.337 486.495C153.636 469.84 140.327 450.103 131.147 428.377C121.624 405.87 116.743 381.673 116.796 357.234C116.742 332.793 121.623 308.593 131.147 286.083C132.662 282.466 134.302 278.915 136.065 275.43C143.07 274.551 150.123 274.111 157.182 274.111C167.114 274.11 177.025 274.988 186.802 276.733C186.59 281.666 186.484 286.644 186.484 291.637C186.35 384.538 222.981 473.718 288.376 539.703C267.751 538.489 247.486 533.75 228.462 525.692ZM309.539 507.06C305.104 502.347 300.838 497.512 296.741 492.557C277.101 468.917 261.052 442.512 249.11 414.192C232.7 375.411 224.285 333.717 224.37 291.607C224.37 290.46 224.37 289.316 224.37 288.175C243.143 296.502 260.212 308.232 274.714 322.773C297.198 345.136 312.765 373.502 319.556 404.478C322.124 416.25 323.414 428.263 323.405 440.311C323.476 463.278 318.772 486.008 309.592 507.06H309.539ZM482.612 363.561C481.454 397.93 470.608 431.273 451.325 459.747C435.654 482.871 414.924 502.124 390.707 516.046L390.464 515.933C384.1 519.579 377.521 522.838 370.763 525.692C362.345 529.254 353.67 532.175 344.811 534.429C343.235 534.833 341.654 535.212 340.068 535.565C339.212 535.762 338.363 535.952 337.507 536.126C337.84 535.52 338.166 534.891 338.484 534.27C338.977 533.323 339.462 532.375 339.932 531.421C345.716 519.852 350.375 507.753 353.843 495.292C358.821 477.405 361.337 458.923 361.322 440.357C361.329 426.85 360.002 413.375 357.359 400.129C343.319 329.918 293.263 272.725 227.28 248.583C230.32 226.251 235.774 204.316 243.548 183.161C245.291 178.428 247.148 173.727 249.118 169.06C252.088 162.028 255.301 155.133 258.756 148.374C258.777 148.34 258.794 148.305 258.809 148.268C260.226 145.32 261.688 142.418 263.204 139.524C275.098 116.875 290.091 95.9959 307.751 77.4882L308.046 77.8445C323.701 96.4088 344.652 121.384 365.52 146.548C376.196 159.429 386.85 172.363 396.761 184.525C415.348 207.34 431.321 227.435 439.952 239.369C441.187 241.082 442.392 242.787 443.574 244.514C455.97 262.51 465.548 280.704 472.041 298.7C476.17 310.046 479.147 321.779 480.93 333.722V333.873C482.388 343.698 482.956 353.634 482.627 363.561H482.612Z" fill="#1F1F1F"/>';
185-
return firebaseIcon;
164+
function getOrCreateEl(id: string): { created: boolean; element: HTMLElement } {
165+
let parentDiv = document.getElementById(id);
166+
let created = false;
167+
if (!parentDiv) {
168+
parentDiv = document.createElement('div');
169+
parentDiv.setAttribute('id', id);
170+
created = true;
171+
}
172+
return { created, element: parentDiv };
186173
}
187-
export function updateStatus(name: string, isRunningEmulator: boolean) {
174+
175+
/**
176+
* Updates Emulator Banner. Primarily used for Firebase Studio
177+
* @param name
178+
* @param isRunningEmulator
179+
* @public
180+
*/
181+
export function updateEmulatorBanner(name: string, isRunningEmulator: boolean) {
188182
if (emulatorStatus[name] === isRunningEmulator) {
189183
// No rerendering required
190184
return;
191185
}
192186
emulatorStatus[name] = isRunningEmulator;
193-
194-
function setDarkMode(el: HTMLElement) {
195-
el.setAttribute(
196-
'style',
197-
'position: fixed; bottom: 0px; border: solid 1px; width: 100%; border-radius: 10px; padding: .5em; text-align: center; background: black; color: white;'
198-
);
199-
}
200-
function setLightMode(el: HTMLElement) {
201-
el.setAttribute(
202-
'style',
203-
'position: fixed; bottom: 0px; border: solid 1px; width: 100%; border-radius: 10px; padding: .5em; text-align: center; background: #e9f1fe; color: black;'
204-
);
187+
if (!areRunningEmulator()) {
188+
tearDown();
189+
return;
205190
}
206-
function setupDom() {
207-
const parentDivId = `__firebase_status`;
208191

209-
let { element: parentDiv, created } = getOrCreate(parentDivId);
192+
function tearDown() {
193+
const divId = `__firebase_status`;
194+
if (typeof document !== 'undefined') {
195+
const element = document.getElementById(divId);
196+
if (element) {
197+
element.remove();
198+
}
199+
}
200+
}
210201

202+
function setupDom() {
203+
const parentDivId = `__firebase__status`;
204+
let { element: parentDiv, created } = getOrCreateEl(parentDivId);
211205
if (created) {
212206
parentDiv.classList.add('firebase-emulator-warning');
213207
document.body.appendChild(parentDiv);
214208
}
215-
window
216-
.matchMedia('(prefers-color-scheme: dark)')
217-
.addEventListener('change', event => {
218-
event.matches ? setDarkMode(parentDiv) : setLightMode(parentDiv);
219-
});
220-
221-
const paragraph = document.createElement('p');
222-
const anchor = document.createElement('a');
223-
if (isRunningEmulator) {
224-
paragraph.innerHTML = 'Running in local emulator';
225-
const firstDashIdx = window.location.host.indexOf('-');
226-
const emulatorHostPort = '4000';
227-
anchor.href = `${emulatorHostPort}-${window.location.host.substring(
228-
firstDashIdx
229-
)}`;
230-
} else {
231-
paragraph.innerHTML = 'Emulator disconnected';
232-
anchor.innerHTML = 'Learn more';
233-
}
234-
const banner = getOrCreate('firebase__banner');
209+
const banner = getOrCreateEl('__firebase__banner');
210+
let firebaseText: HTMLSpanElement =
211+
document.getElementById('__firebase__text') ||
212+
document.createElement('span');
235213
if (banner.created) {
236214
// update styles
237215
const bannerEl = banner.element;
238216
bannerEl.style.display = 'flex';
239217
bannerEl.style.background = '#7faaf0';
240218
bannerEl.style.position = 'absolute';
241-
bannerEl.style.bottom = '0';
242-
const popOver = createPopover();
243-
// TODO(mtewani): Light vs dark mode
244-
const firebaseIcon = createFirebaseEl();
245-
const firebaseText = document.createElement('span');
246-
firebaseText.setAttribute('style', 'align-content: center');
247-
firebaseText.innerText = 'Firebase: ';
248-
const statusEl = document.createElement('span');
249-
updateStatusCount(statusEl);
250-
bannerEl.appendChild(firebaseIcon);
219+
bannerEl.style.bottom = '5px';
220+
bannerEl.style.left = '5px';
221+
bannerEl.style.padding = '.5em';
222+
bannerEl.style.borderRadius = '5px';
251223
bannerEl.appendChild(firebaseText);
252-
bannerEl.appendChild(popOver);
253-
bannerEl.appendChild(statusEl);
254-
banner.element.onclick = () => {
255-
const popover = document.getElementById('firebase__popover');
256-
if (popover) {
257-
if (popover?.style.display === 'none') {
258-
popover.style.display = 'flex';
259-
} else {
260-
popover.style.display = 'none';
261-
}
262-
}
263-
};
264224
document.body.appendChild(banner.element);
265225
}
226+
firebaseText.setAttribute('id', '__firebase__text');
227+
firebaseText.setAttribute('style', 'align-content: center');
228+
firebaseText.innerText = 'Running in this workspace';
266229
}
267230
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
268231
if (document.readyState === 'loading') {
@@ -272,17 +235,3 @@ export function updateStatus(name: string, isRunningEmulator: boolean) {
272235
}
273236
}
274237
}
275-
276-
function updateStatusCount(element: HTMLElement) {
277-
const products = Object.keys(emulatorStatus);
278-
let prodCount = 0;
279-
let localCount = 0;
280-
for(let product in products) {
281-
if(emulatorStatus[product]) {
282-
localCount++;
283-
} else {
284-
prodCount++;
285-
}
286-
}
287-
element.innerText = `Prod (${prodCount}) Local (${localCount})`;
288-
}

0 commit comments

Comments
 (0)