From 28af9c3ed54249b986446a297930b3c04729eb95 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 3 Sep 2026 17:23:36 -0400 Subject: [PATCH 1/5] feat(storybook): add DataVis NITRO WithPerspectives story Bind a localStorage-backed datavis-ace Prefs module to the shared view so the perspective toolbar (Main Perspective dropdown, save/reset/history) renders in Storybook. --- .../DataVisNITRO/DataVisNITRO.stories.tsx | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx index 71072d06..b3ab712a 100644 --- a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx +++ b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx @@ -1,12 +1,25 @@ -import { useContext, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; -import { GridAssistant, type GridAssistantColumn } from '@mieweb/datavis'; +import { + GridAssistant, + type GridAssistantColumn, + type PrefsInstance, +} from '@mieweb/datavis'; +import { Prefs } from 'datavis-ace'; import { DataVisNitroContext, DataVisNitroGrid, DataVisNitroSource, } from './DataVisNITRO'; +// datavis-ace is untyped JS (`Prefs` is built with a runtime `makeSubclass` +// helper), so give it an explicit constructor signature for use below. +const PrefsConstructor = Prefs as unknown as new ( + name: string, + moduleBindings: unknown, + opts: Record +) => PrefsInstance; + const meta: Meta = { title: 'Components/Text & Data Display/DataVis NITRO', component: DataVisNitroGrid, @@ -302,3 +315,65 @@ export const OzwellAssistant: Story = { ), }; + +/** + * Reads the shared view from DataVisNitroContext, binds a localStorage-backed + * Prefs module to it, and passes the module to the grid so the perspective + * toolbar ("Main Perspective" dropdown, save/reset/history buttons) renders. + */ +const PerspectivesGrid = () => { + const view = useContext(DataVisNitroContext); + + const prefs = useMemo(() => { + if (!view) return null; + + const nextPrefs = new PrefsConstructor( + 'mieweb-ui-storybook:employees', + null, + { + autoSave: true, + backend: { + type: 'localStorage', + localStorage: { + key: 'mieweb-ui-storybook:datavis-prefs', + }, + }, + } + ); + + view.setPrefs(nextPrefs); + return nextPrefs; + }, [view]); + + useEffect(() => { + prefs?.prime?.(); + }, [prefs]); + + if (!prefs) return null; + + return ( + + ); +}; + +export const WithPerspectives: Story = { + parameters: { + docs: { + description: { + story: + 'Passing a `prefs` module (a `PrefsInstance` from `datavis-ace`) enables the perspective toolbar: the "Main Perspective" dropdown, save / save-as / reset buttons, and undo/redo history. Perspectives capture the grid configuration (sort, filter, group, pivot, aggregate, column layout) and here persist to `localStorage`. Create the `Prefs` instance, bind it to the shared view with `view.setPrefs(prefs)`, and pass it to ``. In minimal mode the same toolbar appears inside the hamburger menu.', + }, + }, + }, + render: () => ( + + + + ), +}; From c25c97eb26d1e34f98f86356fda700d735e48d2b Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 3 Sep 2026 17:30:47 -0400 Subject: [PATCH 2/5] fix(storybook): move setPrefs side effect out of useMemo Address Copilot review: bind prefs to the view and prime it in a useEffect instead of inside useMemo, which can re-run under StrictMode. --- .../DataVisNITRO/DataVisNITRO.stories.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx index b3ab712a..602c608b 100644 --- a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx +++ b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx @@ -327,27 +327,22 @@ const PerspectivesGrid = () => { const prefs = useMemo(() => { if (!view) return null; - const nextPrefs = new PrefsConstructor( - 'mieweb-ui-storybook:employees', - null, - { - autoSave: true, - backend: { - type: 'localStorage', - localStorage: { - key: 'mieweb-ui-storybook:datavis-prefs', - }, + return new PrefsConstructor('mieweb-ui-storybook:employees', null, { + autoSave: true, + backend: { + type: 'localStorage', + localStorage: { + key: 'mieweb-ui-storybook:datavis-prefs', }, - } - ); - - view.setPrefs(nextPrefs); - return nextPrefs; + }, + }); }, [view]); useEffect(() => { - prefs?.prime?.(); - }, [prefs]); + if (!view || !prefs) return; + view.setPrefs(prefs); + prefs.prime?.(); + }, [view, prefs]); if (!prefs) return null; From b5fcc2f35db659bf0269cd9b8355deac985b6ce8 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 3 Sep 2026 17:50:46 -0400 Subject: [PATCH 3/5] fix(storybook): keep WithPerspectives deterministic in automated runs Address Copilot review: clear the story's localStorage prefs key via a loader when navigator.webdriver is set, so test runners start from a clean default state while preserving persistence for manual browsing. --- .../DataVisNITRO/DataVisNITRO.stories.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx index 602c608b..a7a09182 100644 --- a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx +++ b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx @@ -316,6 +316,8 @@ export const OzwellAssistant: Story = { ), }; +const PREFS_STORAGE_KEY = 'mieweb-ui-storybook:datavis-prefs'; + /** * Reads the shared view from DataVisNitroContext, binds a localStorage-backed * Prefs module to it, and passes the module to the grid so the perspective @@ -332,7 +334,7 @@ const PerspectivesGrid = () => { backend: { type: 'localStorage', localStorage: { - key: 'mieweb-ui-storybook:datavis-prefs', + key: PREFS_STORAGE_KEY, }, }, }); @@ -358,6 +360,17 @@ const PerspectivesGrid = () => { }; export const WithPerspectives: Story = { + loaders: [ + () => { + // Clear saved perspectives in automated runs (test runner, visual + // regression) so the story renders deterministically, while keeping + // localStorage persistence for normal browsing. + if (typeof navigator !== 'undefined' && navigator.webdriver) { + window.localStorage.removeItem(PREFS_STORAGE_KEY); + } + return {}; + }, + ], parameters: { docs: { description: { From be3e13178bcf1c7786073902da781d5a2dd32a0d Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 3 Sep 2026 18:04:23 -0400 Subject: [PATCH 4/5] fix(storybook): guard prefs storage clearing against restricted contexts Address Copilot review: wrap the loader's localStorage.removeItem in try/catch so a SecurityError in restricted/embedded contexts falls back to default perspectives instead of crashing the story. --- src/components/DataVisNITRO/DataVisNITRO.stories.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx index a7a09182..0f292ffe 100644 --- a/src/components/DataVisNITRO/DataVisNITRO.stories.tsx +++ b/src/components/DataVisNITRO/DataVisNITRO.stories.tsx @@ -364,9 +364,14 @@ export const WithPerspectives: Story = { () => { // Clear saved perspectives in automated runs (test runner, visual // regression) so the story renders deterministically, while keeping - // localStorage persistence for normal browsing. - if (typeof navigator !== 'undefined' && navigator.webdriver) { - window.localStorage.removeItem(PREFS_STORAGE_KEY); + // localStorage persistence for normal browsing. localStorage access + // can throw in restricted contexts, so fall back to defaults quietly. + try { + if (typeof navigator !== 'undefined' && navigator.webdriver) { + window.localStorage.removeItem(PREFS_STORAGE_KEY); + } + } catch { + // Storage unavailable — the story just renders its defaults. } return {}; }, From ad4a8648fecbd36112d5d59d80e43f1d80aadb49 Mon Sep 17 00:00:00 2001 From: william garrity Date: Thu, 3 Sep 2026 19:15:10 -0400 Subject: [PATCH 5/5] ci: tolerate npm registry outages in security audit step The npm audit endpoint (registry.npmjs.org/-/npm/v1/security/audits) has intermittent outages that fail CI with ERR_SOCKET_TIMEOUT even though no vulnerability exists (run #1915 failed twice on this). Detect network errors in the audit output and emit a warning instead of failing; genuine high-severity findings still fail the job. --- .github/workflows/ci.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb4e2a0..f21e5709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -247,7 +247,19 @@ jobs: cache: 'pnpm' - name: Run security audit - run: pnpm audit --audit-level=high --prod + # The npm audit endpoint has intermittent outages (ERR_SOCKET_TIMEOUT). + # Treat registry connectivity failures as a warning instead of failing + # the build; real vulnerability findings still fail. + run: | + set +e + output=$(pnpm audit --audit-level=high --prod 2>&1) + exit_code=$? + echo "$output" + if [ $exit_code -ne 0 ] && echo "$output" | grep -qE 'ERR_SOCKET_TIMEOUT|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|FetchError'; then + echo "::warning::npm audit registry endpoint unreachable — skipping audit (network error, not a vulnerability finding)" + exit 0 + fi + exit $exit_code - name: Run dependency license check run: pnpm dlx license-checker --onlyAllow 'MIT;Apache-2.0;ISC;BSD;BSD-2-Clause;BSD-3-Clause;Apache;Unlicense' \ No newline at end of file