mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(ui): name the missing provider key on a key-gated layer row (#575)
Supersedes #296 by @Matthew-Selvam, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Co-authored-by: Matthew-Selvam <[email protected]>
This commit is contained in:
1 parent
7fa0cfbb26
commit
6fa25c349a
8 files changed
+206
-3
No files matched your search
@@ -166,6 +166,12 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
the bundled place. Anything else, including anything malformed, goes to the
|
||||
existing geocoders unchanged. Degrees/minutes/seconds and grid references are
|
||||
not parsed and fall through the same way (#388 — thanks @KuraPiee).
|
||||
- A data-layer control a provider key is holding back now names that key. With
|
||||
no FIRMS key the fire layer's control read KEY REQUIRED without saying which
|
||||
key or where to put it; it now reads "Needs FIRMS_MAP_KEY — add it in Provider
|
||||
Settings", on the control and in its accessible name. A layer that needs no
|
||||
key, or already holds one, carries no such text, and an unrecognised key name
|
||||
produces none rather than a guess (#296 — thanks @Matthew-Selvam).
|
||||
|
||||
- Draped annotation geometry — area fills and outlines, routes and arrows —
|
||||
classifies onto terrain as well as 3D tiles. On a keyless boot, where Cesium's
|
||||
|
||||
@@ -787,7 +787,13 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
||||
> is present — everyone gets the same tile.
|
||||
>
|
||||
> Keyless, the honest surface is the **layer row**, which reads
|
||||
> `UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED`, and the earthquakes half
|
||||
> `UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED`. Its control also names the
|
||||
> key: hovering it, and its accessible name, read
|
||||
> `Needs FIRMS_MAP_KEY — add it in Provider Settings`. A layer declares which
|
||||
> key it needs as a key-registry id and reports `stats.keyRequired` while that
|
||||
> key is absent; the panel builds the text from the pair, and produces none for
|
||||
> a layer that needs no key, already holds one, or names a key the registry does
|
||||
> not recognise. The earthquakes half
|
||||
> still delivers in full. The shared loading reducer now treats an explicitly
|
||||
> declared missing optional key as a configured terminal state rather than a
|
||||
> failed multi-layer mission, so the global chip completes without showing
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"scripts/qa-map-source-controls.mjs",
|
||||
"src/ui/layers.js",
|
||||
"src/ui/layerPanel.js",
|
||||
"src/ui/layerKeyRequirement.test.mjs",
|
||||
"src/ui/clearLayersControl.js",
|
||||
"scripts/qa-layer-panel.mjs",
|
||||
"scripts/qa-directions.mjs",
|
||||
|
||||
@@ -353,7 +353,8 @@
|
||||
"src/ui/layerPanel.js",
|
||||
"src/ui/clearLayersControl.js",
|
||||
"src/loadingFeedback.js",
|
||||
"src/data/installationFeedback.js"
|
||||
"src/data/installationFeedback.js",
|
||||
"src/keySetupCore.mjs"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
|
||||
@@ -2200,6 +2200,10 @@ export class LayerLifecycle {
|
||||
icon: entry.module.icon,
|
||||
source: entry.module.source,
|
||||
showInTogglePanel: entry.module.showInTogglePanel !== false,
|
||||
// Registry id of the provider key this layer needs, if any (mirrors
|
||||
// showInTogglePanel). The layer reports stats.keyRequired while that
|
||||
// key is absent; the panel builds the guidance text from the pair.
|
||||
requiresKeyId: entry.module.requiresKeyId || null,
|
||||
enabled: entry.enabled,
|
||||
lifecycleState: entry.lifecycleState,
|
||||
lifecycleUncertain: entry.lifecycleUncertain,
|
||||
|
||||
@@ -38,6 +38,12 @@ export function createQueries({
|
||||
|
||||
source,
|
||||
|
||||
// The one data-layer control a provider key gates: the proxy answers
|
||||
// 503 {error:'no_key'} without a FIRMS key. Declared as a key-registry id
|
||||
// rather than an env-var string, so the panel can name the key from the
|
||||
// one place that owns what each key is called.
|
||||
requiresKeyId: 'firms',
|
||||
|
||||
// Live layer: the manager calls update() every 10 minutes while enabled,
|
||||
// which refetches through the /api/firms proxy (the proxy's 30 min TTL —
|
||||
// not this interval — is what protects the upstream FIRMS quota).
|
||||
@@ -75,6 +81,11 @@ export function createQueries({
|
||||
lastUpdate: layerState._lastUpdate,
|
||||
loading: layerState._loading,
|
||||
stale: layerState._stale,
|
||||
// The machine-readable half of the keyless state, ahead of the human
|
||||
// strings below: without it "no key configured" is indistinguishable
|
||||
// from a broken feed, and the row reads as a fault instead of a step
|
||||
// the operator can take.
|
||||
keyRequired: layerState._keyRequired,
|
||||
error: layerState._keyRequired
|
||||
? 'KEY REQUIRED'
|
||||
: layerState._stale
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// NAMING THE MISSING KEY — a control a provider key holds back must say which
|
||||
// key, or the operator is left with a dead row and no next step. The three
|
||||
// halves are pinned here: the layer declares which key it needs and reports
|
||||
// that it is missing, the manager publishes that on the row, and the panel
|
||||
// turns the pair into guidance that names the environment variable.
|
||||
//
|
||||
// Run with: npm test
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { layerKeyRequirementTooltip } from './layerPanel.js';
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import { KEY_SETUP_KEYS } from '../keySetupCore.mjs';
|
||||
import { createFirmsState } from '../layers/firms/state.js';
|
||||
import { createModel } from '../layers/firms/model.js';
|
||||
import { createQueries } from '../layers/firms/queries.js';
|
||||
|
||||
/** The fire layer's query half, with the scene services it never calls stubbed. */
|
||||
function firmsQueries() {
|
||||
const services = {
|
||||
render: { governorRequestRender() {} },
|
||||
anchors: { fireAnchorHeight: () => 0, FIRE_ANCHOR_LIFT_M: 0 },
|
||||
overlays: {},
|
||||
sprites: {},
|
||||
context: {},
|
||||
picking: {},
|
||||
focus: {},
|
||||
};
|
||||
const config = { id: 'local-firms', name: 'FIRMS Active Fires' };
|
||||
const layerState = createFirmsState({ services, config });
|
||||
const components = {};
|
||||
const context = {
|
||||
layerState,
|
||||
services,
|
||||
components,
|
||||
config,
|
||||
feed: { getSnapshot: async () => ({}) },
|
||||
};
|
||||
components.model = createModel(context);
|
||||
return { layerState, methods: createQueries(context).methods };
|
||||
}
|
||||
|
||||
test('the fire layer names the key it needs and reports when it is missing', () => {
|
||||
const { layerState, methods } = firmsQueries();
|
||||
assert.equal(methods.requiresKeyId, 'firms');
|
||||
assert.ok(
|
||||
KEY_SETUP_KEYS.some((entry) => entry.id === 'firms'),
|
||||
'the declared id must exist in the key registry',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
methods.getStats().keyRequired,
|
||||
false,
|
||||
'no key is missing until the feed says so',
|
||||
);
|
||||
|
||||
layerState._keyRequired = true;
|
||||
const stats = methods.getStats();
|
||||
assert.equal(stats.keyRequired, true);
|
||||
assert.equal(
|
||||
stats.error,
|
||||
'KEY REQUIRED',
|
||||
'the human string the row already shows is unchanged',
|
||||
);
|
||||
});
|
||||
|
||||
test('the guidance names the environment variable and where to set it', () => {
|
||||
const text = layerKeyRequirementTooltip({
|
||||
requiresKeyId: 'firms',
|
||||
stats: { keyRequired: true },
|
||||
});
|
||||
assert.match(text, /FIRMS_MAP_KEY/);
|
||||
assert.match(text, /Provider Settings/);
|
||||
});
|
||||
|
||||
test('guidance appears only for a key that is actually missing and actually named', () => {
|
||||
for (const layer of [
|
||||
undefined,
|
||||
{},
|
||||
{ requiresKeyId: 'firms' },
|
||||
{ requiresKeyId: 'firms', stats: {} },
|
||||
{ requiresKeyId: 'firms', stats: { keyRequired: false } },
|
||||
// Truthy but not the boolean the contract asks for: a stray string must
|
||||
// not be read as "the key is missing".
|
||||
{ requiresKeyId: 'firms', stats: { keyRequired: 'yes' } },
|
||||
// Missing key, but the layer never said which one.
|
||||
{ stats: { keyRequired: true } },
|
||||
{ requiresKeyId: '', stats: { keyRequired: true } },
|
||||
{ requiresKeyId: ' ', stats: { keyRequired: true } },
|
||||
// An id the registry does not know must not be guessed at: guidance
|
||||
// naming the wrong variable sends the operator to the wrong provider.
|
||||
{ requiresKeyId: 'not-a-provider', stats: { keyRequired: true } },
|
||||
]) {
|
||||
assert.equal(
|
||||
layerKeyRequirementTooltip(layer),
|
||||
'',
|
||||
`${JSON.stringify(layer)} must produce no guidance`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the manager publishes the declared key id on the row', async () => {
|
||||
const manager = new DataLayerManager();
|
||||
const base = {
|
||||
name: 'Test layer',
|
||||
icon: '•',
|
||||
source: 'Test',
|
||||
init() {},
|
||||
enable() {},
|
||||
disable() {},
|
||||
update() {},
|
||||
destroy() {},
|
||||
};
|
||||
manager.register({
|
||||
...base,
|
||||
id: 'gated',
|
||||
requiresKeyId: 'firms',
|
||||
getStats: () => ({ count: 0, keyRequired: true }),
|
||||
});
|
||||
manager.register({
|
||||
...base,
|
||||
id: 'ungated',
|
||||
getStats: () => ({ count: 3, lastUpdate: Date.now() }),
|
||||
});
|
||||
|
||||
// The manager consults a layer's getStats() only once it is initialized, so
|
||||
// the rows are read the way the panel reads them: after the layers are on.
|
||||
await manager.setEnabled('gated', true);
|
||||
await manager.setEnabled('ungated', true);
|
||||
|
||||
const rows = manager.getAll();
|
||||
const gated = rows.find((row) => row.id === 'gated');
|
||||
const ungated = rows.find((row) => row.id === 'ungated');
|
||||
assert.equal(gated.requiresKeyId, 'firms');
|
||||
assert.equal(gated.stats.keyRequired, true);
|
||||
assert.match(layerKeyRequirementTooltip(gated), /FIRMS_MAP_KEY/);
|
||||
|
||||
assert.equal(
|
||||
ungated.requiresKeyId,
|
||||
null,
|
||||
'a layer that needs no key declares none',
|
||||
);
|
||||
assert.equal(layerKeyRequirementTooltip(ungated), '');
|
||||
});
|
||||
+32
-1
@@ -1,4 +1,5 @@
|
||||
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
|
||||
import { keySetupRequirement } from '../keySetupCore.mjs';
|
||||
const FEED_STATE_LABELS = Object.freeze({
|
||||
nominal: 'ON',
|
||||
loading: 'LOADING',
|
||||
@@ -8,6 +9,26 @@ const FEED_STATE_LABELS = Object.freeze({
|
||||
unavailable: 'UNAVAILABLE',
|
||||
});
|
||||
|
||||
/**
|
||||
* Guidance for a control a missing provider key is holding back.
|
||||
*
|
||||
* The key registry already owns what each key is called and which environment
|
||||
* variables enable it, so a layer only declares WHICH key it needs
|
||||
* (`requiresKeyId`) and reports `stats.keyRequired` while that key is absent.
|
||||
* Naming the variable turns an unexplained dead control into a next step.
|
||||
*
|
||||
* An unnamed or unknown key returns '' rather than guessing: guidance naming
|
||||
* the wrong variable sends the operator to the wrong provider.
|
||||
*
|
||||
* @param {object} [layer] Row from the layer manager's getAll().
|
||||
* @returns {string} Guidance text, or '' when no key guidance applies.
|
||||
*/
|
||||
export function layerKeyRequirementTooltip(layer = {}) {
|
||||
if (layer?.stats?.keyRequired !== true) return '';
|
||||
const requiresKeyId = String(layer.requiresKeyId || '').trim();
|
||||
return requiresKeyId ? keySetupRequirement(requiresKeyId) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize heterogeneous layer stats into one honest control-chip state.
|
||||
* @param {object|null} stats Layer getStats() result.
|
||||
@@ -511,7 +532,17 @@ export class LayerPanel {
|
||||
: layer.enabled
|
||||
? FEED_STATE_LABELS[feedState]
|
||||
: 'OFF';
|
||||
button.setAttribute('aria-label', `${layer.name}: ${button.textContent}`);
|
||||
const keyGuidance = layerKeyRequirementTooltip(layer);
|
||||
// Name the missing key on the control itself: a row reading KEY REQUIRED
|
||||
// without saying WHICH key leaves a dead control and no next step. Empty
|
||||
// when the layer needs no key, or already has one.
|
||||
button.title = keyGuidance;
|
||||
button.setAttribute(
|
||||
'aria-label',
|
||||
keyGuidance
|
||||
? `${layer.name}: ${button.textContent}. ${keyGuidance}`
|
||||
: `${layer.name}: ${button.textContent}`,
|
||||
);
|
||||
}
|
||||
|
||||
_formatCount(n) {
|
||||
|
||||
Reference in new issue
Block a user