Skip to content

Commit 4ace6c2

Browse files
committed
Support for displaying "preview" bucket data from RS.
"preview" is for changes that are still pending review. This patch also reefactors how the Remote Settings urls are built and consolidates the logic between the NodeJS script and the main web app.
1 parent b6a8ee2 commit 4ace6c2

File tree

5 files changed

+171
-62
lines changed

5 files changed

+171
-62
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev": "vite",
1010
"build": "tsc && vite build",
1111
"preview": "vite preview",
12-
"update-types": "node scripts/updateTypesFromSchema.cjs"
12+
"update-types": "node scripts/updateTypesFromSchema.mjs"
1313
},
1414
"author": "Emma Zuehlcke",
1515
"license": "MPL-2.0",

scripts/rs-config.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import type { RS_BASE_URLS } from "./rs-config.js";
6+
7+
export type RSEnvironment = "dev" | "stage" | "prod";
8+
9+
export function isRSEnvValid(env: string | null): env is RSEnvironment;
10+
export function getRSEndpoint(env: RSEnvironment, isPreview: boolean): URL;
11+
export function getRSEndpointMeta(env: RSEnvironment, isPreview: boolean): URL;

scripts/rs-config.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
// Base URLs for different Remote Settings environments
6+
export const RS_BASE_URLS = {
7+
prod: "https://firefox.settings.services.mozilla.com",
8+
stage: "https://firefox.settings.services.allizom.org",
9+
dev: "https://remote-settings-dev.allizom.org",
10+
};
11+
12+
// Collection name
13+
const RS_COLLECTION = "url-classifier-exceptions";
14+
15+
export function isRSEnvValid(env) {
16+
return env && Object.keys(RS_BASE_URLS).includes(env);
17+
}
18+
19+
/**
20+
* Build a Remote Settings URL for a given environment and bucket
21+
* @param {string} baseUrl - The base URL for the Remote Settings environment.
22+
* @param {string} bucket - The bucket name.
23+
* @param {string} collection - The collection name.
24+
* @param {string} suffix - The suffix to add to the URL.
25+
* @returns {URL} The Remote Settings URL.
26+
*/
27+
function buildRSUrl(baseUrl, bucket, collection, suffix) {
28+
if (!baseUrl || !bucket || !collection) {
29+
throw new Error("Invalid parameters");
30+
}
31+
32+
let urlStr = `${baseUrl}/v1/buckets/${bucket}/collections/${collection}`;
33+
34+
if (suffix) {
35+
urlStr += `/${suffix}`;
36+
}
37+
38+
return new URL(urlStr);
39+
}
40+
41+
/**
42+
* Get the Remote Settings endpoint for records.
43+
* @param {string} env - The environment to use.
44+
* @param {boolean} isPreview - Whether to use the preview bucket.
45+
* @returns {URL} The Remote Settings URL.
46+
*/
47+
export function getRSEndpoint(env, isPreview) {
48+
return buildRSUrl(
49+
RS_BASE_URLS[env],
50+
isPreview ? "main-preview" : "main",
51+
RS_COLLECTION,
52+
"records",
53+
);
54+
}
55+
56+
/**
57+
* Get the Remote Settings endpoint for collection metadata.
58+
* @param {string} env - The environment to use.
59+
* @param {boolean} isPreview - Whether to use the preview bucket.
60+
* @returns {URL} The Remote Settings URL.
61+
*/
62+
export function getRSEndpointMeta(env, isPreview) {
63+
return buildRSUrl(RS_BASE_URLS[env], isPreview ? "main-preview" : "main", RS_COLLECTION);
64+
}

scripts/updateTypesFromSchema.cjs renamed to scripts/updateTypesFromSchema.mjs

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,31 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
const fs = require("fs");
6-
const path = require("path");
7-
const { compile } = require("json-schema-to-typescript");
8-
require("dotenv").config();
9-
10-
const RS_ENDPOINTS = {
11-
prod: "https://firefox.settings.services.mozilla.com",
12-
stage: "https://firefox.settings.services.allizom.org",
13-
dev: "https://remote-settings-dev.allizom.org",
14-
};
5+
import fs from "fs";
6+
import path from "path";
7+
import { fileURLToPath } from "url";
8+
import { compile } from "json-schema-to-typescript";
9+
import { getRSEndpointMeta } from "./rs-config.js";
10+
import "dotenv/config";
1511

1612
/**
1713
* Get the Remote Settings endpoint to use.
18-
* Can be overridden by setting VITE_RS_ENVIRONMENT in .env to one of: "prod", "stage", "dev"
14+
* Can be overridden by setting VITE_RS_ENVIRONMENT in .env to one of: "prod",
15+
* "prod_preview", "stage", "stage_preview", "dev", "dev_preview".
1916
* Defaults to "prod" if not set or invalid
2017
*/
21-
function getRSEndpoint() {
22-
const env = process.env.VITE_RS_ENVIRONMENT;
23-
return RS_ENDPOINTS[env] || RS_ENDPOINTS.prod;
18+
function getRSUrl() {
19+
const env = process.env.VITE_RS_ENVIRONMENT || "prod";
20+
return getRSEndpointMeta(env, false);
2421
}
2522

2623
async function generateTypes() {
2724
try {
28-
// Fetch the schema from Remote Settings
29-
const rsOrigin = getRSEndpoint();
30-
console.info(`Fetching schema from ${rsOrigin}`);
25+
// Fetch the schema from Remote Settings collection metadata
26+
const rsUrl = getRSUrl();
27+
console.info(`Fetching schema from ${rsUrl.toString()}`);
3128

32-
const response = await fetch(
33-
`${rsOrigin}/v1/buckets/main/collections/url-classifier-exceptions`,
34-
);
29+
const response = await fetch(rsUrl.toString());
3530
const data = await response.json();
3631
const schema = data.data.schema;
3732

@@ -44,6 +39,8 @@ async function generateTypes() {
4439
},
4540
});
4641

42+
const __filename = fileURLToPath(import.meta.url);
43+
const __dirname = path.dirname(__filename);
4744
const srcDir = path.join(__dirname, "..", "src");
4845

4946
// Write the types to a file

src/app.ts

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { LitElement, html, css } from "lit";
66
import { customElement, state } from "lit/decorators.js";
77
import { ExceptionListEntry, BugMetaMap } from "./types";
8+
import { getRSEndpoint, RSEnvironment, isRSEnvValid } from "../scripts/rs-config.js";
89
import "./exceptions-table/exceptions-table";
910
import "./exceptions-table/top-exceptions-table";
1011
import "./github-corner";
@@ -13,51 +14,52 @@ const GITHUB_URL = "https://github.com/mozilla/url-classifier-exceptions-ui";
1314

1415
// Query parameter which can be used to override the RS environment.
1516
const QUERY_PARAM_RS_ENV = "rs_env";
16-
17-
// The available Remote Settings endpoints.
18-
const RS_ENDPOINTS = {
19-
prod: "https://firefox.settings.services.mozilla.com",
20-
stage: "https://firefox.settings.services.allizom.org",
21-
dev: "https://remote-settings-dev.allizom.org",
22-
} as const;
23-
24-
type RSEndpointKey = keyof typeof RS_ENDPOINTS;
17+
const QUERY_PARAM_RS_USE_PREVIEW = "rs_preview";
2518

2619
/**
2720
* Get the RS environment from URL parameters, falling back to the defaults if
2821
* not specified.
2922
* @returns The RS environment key
3023
*/
31-
function getRsEnv(): RSEndpointKey {
24+
function getRsEnv(): { env: RSEnvironment; usePreview: boolean } {
25+
// Check if the environment is specified in the URL.
3226
const params = new URLSearchParams(window.location.search);
3327
const env = params.get(QUERY_PARAM_RS_ENV);
34-
if (env && Object.keys(RS_ENDPOINTS).includes(env)) {
35-
return env as RSEndpointKey;
28+
const usePreview = params.get(QUERY_PARAM_RS_USE_PREVIEW) === "true";
29+
if (isRSEnvValid(env)) {
30+
return { env: env as RSEnvironment, usePreview };
3631
}
37-
// Fall back to build env configuration or if env is not set, the default of "prod".
38-
return (import.meta.env.VITE_RS_ENVIRONMENT as RSEndpointKey) || "prod";
32+
33+
// Fall back to build-time environment variable.
34+
let viteEnv = import.meta.env.VITE_RS_ENVIRONMENT;
35+
if (isRSEnvValid(viteEnv)) {
36+
return { env: viteEnv as RSEnvironment, usePreview: false };
37+
}
38+
39+
// Otherwise default to prod, non preview.
40+
return { env: "prod", usePreview: false };
3941
}
4042

4143
/**
4244
* Get the URL for the records endpoint for a given Remote Settings environment.
43-
* @param rsOrigin The origin of the Remote Settings environment.
45+
* @param rsUrl The URL of the Remote Settings environment.
4446
* @returns The URL for the records endpoint.
4547
*/
46-
function getRecordsUrl(rsOrigin: string): string {
48+
function getRecordsUrl(rsUrl: string): string {
4749
// Allow ENV to override the URL for testing.
4850
if (import.meta.env.VITE_RS_RECORDS_URL) {
4951
return import.meta.env.VITE_RS_RECORDS_URL;
5052
}
51-
return `${rsOrigin}/v1/buckets/main/collections/url-classifier-exceptions/records`;
53+
return rsUrl;
5254
}
5355

5456
/**
5557
* Fetch the records from the Remote Settings environment.
56-
* @param rsOrigin The origin of the Remote Settings environment.
58+
* @param rsUrl The URL of the Remote Settings environment.
5759
* @returns The records.
5860
*/
59-
async function fetchRecords(rsOrigin: string): Promise<ExceptionListEntry[]> {
60-
const response = await fetch(getRecordsUrl(rsOrigin));
61+
async function fetchRecords(rsUrl: string): Promise<ExceptionListEntry[]> {
62+
const response = await fetch(getRecordsUrl(rsUrl));
6163
if (!response.ok) {
6264
throw new Error(`Failed to fetch records: ${response.statusText}`);
6365
}
@@ -124,7 +126,13 @@ export class App extends LitElement {
124126
// at build time. The user can change this via a dropdown. The user can also
125127
// override the environment via a query parameter.
126128
@state()
127-
rsEnv: RSEndpointKey = getRsEnv();
129+
rsEnv: RSEnvironment = "prod";
130+
131+
// Whether to use the preview environment. This bucket includes changes that
132+
// are still pending review. The user can change this via a checkbox. It can
133+
// also be overridden via a query parameter.
134+
@state()
135+
rsEnvUsePreview: boolean = false;
128136

129137
// Holds all fetched records.
130138
@state()
@@ -187,6 +195,12 @@ export class App extends LitElement {
187195
*/
188196
connectedCallback() {
189197
super.connectedCallback();
198+
199+
// Set the initial RS environment and preview setting.
200+
let { env, usePreview } = getRsEnv();
201+
this.rsEnv = env;
202+
this.rsEnvUsePreview = usePreview;
203+
190204
this.init();
191205
}
192206

@@ -196,7 +210,9 @@ export class App extends LitElement {
196210
async init() {
197211
try {
198212
this.loading = true;
199-
this.records = await fetchRecords(RS_ENDPOINTS[this.rsEnv]);
213+
214+
const urlStr = getRSEndpoint(this.rsEnv, this.rsEnvUsePreview).toString();
215+
this.records = await fetchRecords(urlStr);
200216

201217
// Spot check if the format is as expected.
202218
if (this.records.length && this.records[0].bugIds == null) {
@@ -227,6 +243,31 @@ export class App extends LitElement {
227243
return new Set(this.records.flatMap((record) => record.bugIds || [])).size;
228244
}
229245

246+
/**
247+
* Handle changes to RS environment settings via the UI.
248+
* @param event The change event either the dropdown or the checkbox.
249+
*/
250+
private handleRSEnvChange(event: Event) {
251+
const target = event.target as HTMLSelectElement | HTMLInputElement;
252+
253+
if (target.id === "rs-env") {
254+
this.rsEnv = (target as HTMLSelectElement).value as RSEnvironment;
255+
// Reset preview setting when environment changes.
256+
this.rsEnvUsePreview = false;
257+
} else if (target.id === "rs-env-preview") {
258+
this.rsEnvUsePreview = (target as HTMLInputElement).checked;
259+
}
260+
261+
// Update URL parameters to reflect current settings
262+
const url = new URL(window.location.href);
263+
url.searchParams.set(QUERY_PARAM_RS_ENV, this.rsEnv);
264+
url.searchParams.set(QUERY_PARAM_RS_USE_PREVIEW, this.rsEnvUsePreview.toString());
265+
window.history.pushState({}, "", url);
266+
267+
// Fetch the records again with the new settings
268+
this.init();
269+
}
270+
230271
/**
231272
* Handle anchor navigation. We need to do this via JS because native navigation
232273
* does not traverse shadow DOM.
@@ -370,32 +411,28 @@ export class App extends LitElement {
370411
<footer>
371412
<p>
372413
<label for="rs-env">Remote Settings Environment:</label>
373-
<select
374-
id="rs-env"
375-
@change=${(e: Event) => {
376-
const newEnv = (e.target as HTMLSelectElement).value as RSEndpointKey;
377-
this.rsEnv = newEnv;
378-
379-
// When the env changes reflect the update in the URL.
380-
// Update URL parameter without reloading the page
381-
const url = new URL(window.location.href);
382-
url.searchParams.set(QUERY_PARAM_RS_ENV, newEnv);
383-
window.history.pushState({}, "", url);
384-
385-
// Fetch the records again.
386-
this.init();
387-
}}
388-
>
414+
<select id="rs-env" @change=${this.handleRSEnvChange}>
389415
<option value="prod" ?selected=${this.rsEnv === "prod"}>Prod</option>
390416
<option value="stage" ?selected=${this.rsEnv === "stage"}>Stage</option>
391417
<option value="dev" ?selected=${this.rsEnv === "dev"}>Dev</option>
392418
</select>
419+
<br />
420+
<label for="rs-env-preview">Show changes pending review:</label>
421+
<input
422+
id="rs-env-preview"
423+
type="checkbox"
424+
?checked=${this.rsEnvUsePreview}
425+
@change=${this.handleRSEnvChange}
426+
/>
393427
</p>
394428
<p>
395429
Data source:
396-
<a href="${getRecordsUrl(RS_ENDPOINTS[this.rsEnv])}"
397-
>${getRecordsUrl(RS_ENDPOINTS[this.rsEnv])}</a
398-
>.
430+
${(() => {
431+
const recordsUrl = getRecordsUrl(
432+
getRSEndpoint(this.rsEnv, this.rsEnvUsePreview).toString(),
433+
);
434+
return html`<a href="${recordsUrl}">${recordsUrl}</a>`;
435+
})()}.
399436
</p>
400437
</footer>
401438
<!-- Show a link to the repository in the top right corner -->

0 commit comments

Comments
 (0)