Skip to content

phase-1d: jakarta migration (Spring 6, Jersey 3, Jetty 12)#759

Open
buggtb wants to merge 35 commits intodevelopmentfrom
phase-1d-jakarta-springboot
Open

phase-1d: jakarta migration (Spring 6, Jersey 3, Jetty 12)#759
buggtb wants to merge 35 commits intodevelopmentfrom
phase-1d-jakarta-springboot

Conversation

@buggtb
Copy link
Copy Markdown
Member

@buggtb buggtb commented Apr 19, 2026

Summary

  • Migrate Saiku + local forks (mondrian, olap4j-xmlaserver) to jakarta namespaces
  • Bump Spring 4.3 → 6.1, Spring Security 4.2 → 6.3, Jersey 1.19 → 3.1, Jetty 10 → 12, JAXB 2.3 → 4.0
  • Drop dead Jackrabbit WebDAV servlet and javax.servlet H2Console

Fork changes (installed locally)

  • mondrian 4.8.0.0-SAIKU → 4.8.1.0-SAIKU-jakarta (on branch jakarta-servlet of ~/Projects/saiku/mondrian-saiku)
  • olap4j-xmlaserver 1.2.0-spicule-1 → 1.3.0-spicule-jakarta (on branch jakarta-servlet of ~/Projects/olap4j-xmlaserver)

Source migration (sed-driven)

  • javax.servlet.*jakarta.servlet.*
  • javax.ws.rs.*jakarta.ws.rs.*
  • javax.xml.bind.*jakarta.xml.bind.*
  • com.sun.jersey.multipartorg.glassfish.jersey.media.multipart

Status

  • mvn -DskipTests install green across all modules
  • GET /rest/saiku/info → 200
  • POST /rest/saiku/session (login) → 200 with {"isadmin":true,"roles":["ROLE_ADMIN","ROLE_USER"]}
  • ⚠️ GET /rest/saiku/admin/datasources → 401 — Spring Security 6 session-context persistence on subsequent requests needs follow-up. The HttpSessionSecurityContextRepository.saveContext is called on login but the filter chain isn't restoring it; likely a <security:http> namespace vs SecurityFilterChain-bean issue in Spring Security 6.

Test plan

  • Local mvn install succeeds
  • mvn jetty:run boots without ContextLoader errors
  • REST /info returns 200
  • REST login accepts credentials and returns session object
  • Authenticated admin endpoints return expected payload (follow-up: Spring Security 6 filter chain)

🤖 Generated with Claude Code

buggtb and others added 30 commits April 19, 2026 11:18
… namespaces

Forks bumped:
- mondrian: 4.8.0.0-SAIKU → 4.8.1.0-SAIKU-jakarta
- olap4j-xmlaserver: 1.2.0-spicule-1 → 1.3.0-spicule-jakarta

Dependency upgrades:
- Spring 4.3.30 → 6.1.14
- Spring Security 4.2.20 → 6.3.4
- Jersey 1.19 → Jersey 3.1.9 (jakarta, glassfish)
- javax.servlet-api 3.1 → jakarta.servlet-api 6.0
- jakarta.xml.bind-api 2.3 → 4.0; jaxb-runtime 4.0.5
- commons-io 2.4 → 2.17
- jetty maven plugin: 6.1 → jetty-ee10 12.0.16

Source migration:
- javax.servlet.* → jakarta.servlet.*
- javax.ws.rs.* → jakarta.ws.rs.*
- javax.xml.bind.* → jakarta.xml.bind.*
- com.sun.jersey.multipart → org.glassfish.jersey.media.multipart
- Removed @component from Jersey resource classes (XML beans are source of truth)
- web.xml: Jersey 1 SpringServlet → Jersey 3 ServletContainer with MultiPartFeature
- web.xml: dropped dead SaikuWebdavServlet (Jackrabbit) + H2Console (javax.servlet)
- Spring XSDs: pinned versions → unversioned
- SessionService now explicitly saves SecurityContext via HttpSessionSecurityContextRepository (Spring Security 6 behavioural change)
- users.properties passwords prefixed with {noop} for Spring Security 6
- BasicRepositoryResource2: finished VFS → java.io.File refactor
- JdbcUserDAO: fixed classpath ../database-queries.properties lookup
- Removed dead MondrianVFS bean from saiku-beans.xml

Status: local build green, REST GET /info + POST /session (login) returning 200 with admin roles. Spring Security 6 session persistence on authenticated endpoints needs follow-up — GET /session works logged-in but hasRole-gated /rest/saiku/admin/* still returns 401.
- Add SaikuJerseyApplication ResourceConfig that pulls @Path-annotated Spring
  beans from root WebApplicationContext (skipping CGLIB scopedTarget.* entries)
- web.xml: swap Jersey package-scan for jakarta.ws.rs.Application init-param
- saiku-beans.xml: add <aop:scoped-proxy/> to all session/request scoped beans
  so singletons can reference them from outside an active scope
- Make no-arg constructors public on resource + service classes CGLIB needs to
  subclass for scope proxies (OlapQueryService, ThinQueryService,
  OlapDiscoverService + dependent resources)
- Exclude transitive javax.servlet / jersey 1.x / jetty 6 from hadoop-common
- Drop jackson-jaxrs-json-provider (javax); jersey-media-json-jackson handles it

Verified end-to-end:
- GET  /rest/saiku/info → 200 []
- POST /rest/saiku/session (admin/admin) → 200 + JSESSIONID
- GET  /rest/saiku/session → 200 with isadmin:true, roles [ROLE_ADMIN,ROLE_USER]
- GET  /rest/saiku/admin/datasources → 200 JSON array of cube connections
- New saiku-launcher module produces a single runnable jar (saiku-3.17.jar)
  bundling the full Saiku webapp + embedded Jetty 12
- Picocli CLI with 'serve' subcommand: --port, --host, --context, --home
- Launcher creates saiku home directory structure (data/, repository/data/,
  logs/, plugins/) and sets -Dsaiku.home for Spring placeholders
- saiku-beans.properties paths rewritten to ${saiku.home}/... tokens
- <context:property-placeholder> gets system-properties-mode=OVERRIDE so
  -Dsaiku.home wins
- Database.initDB now expandSaikuHome()-s context-param URLs so the legacy
  ../../data relative paths resolve under the runtime home dir
- Foodmart/Earthquakes sample-data loaders catch missing files instead of
  failing context init — allows shipping without the demo SQL/XML
- Make ClassPathRepositoryManager.bootstrap re-entrancy-safe
  (bootstrapping flag) to prevent infinite recursion when data dir absent
- Make no-arg ctors public on resource + olap service classes so Spring CGLIB
  scoped-proxy can subclass them
- sessionRepo demoted from session-scoped to singleton — legitimately a
  container for the current HttpSession, not per-session state
- Add <aop:scoped-proxy/> to every session/request scoped bean so singletons
  can hold a reference to them
- SaikuJerseyApplication filters scopedTarget.* beans so only the proxies
  (not the real scoped beans) are registered with Jersey at init time

Verified:
- `mvn install` green across all modules (incl. saiku-launcher)
- `java -jar saiku-launcher/target/saiku-3.17.jar serve` boots cleanly in
  an empty working dir
- `POST /rest/saiku/session` → 200 admin login, `GET /rest/saiku/session`
  returns `{isadmin:true, roles:[ROLE_ADMIN, ROLE_USER], username:admin}`
- Multi-stage build: maven:3.9-temurin-21 → distroless java21 nonroot
- Bundles saiku-launcher fat JAR as /app/saiku.jar, SAIKU_HOME volume
- release.yml on tag: build JAR, build+push multi-arch image to GHCR,
  publish GitHub Release with JAR asset and generated notes
- workflow_dispatch entrypoint for manual releases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add saiku-native org.saiku.repository.RepositoryException and
  PathNotFoundException; swap all javax.jcr.* imports across
  IRepositoryManager, IDatasourceManager, ClassPathRepositoryManager,
  ClassPathResourceDatasourceManager, RepositoryDatasourceManager,
  AdminResource, MockRepositoryManager.
- Strip Jackrabbit Node-based methods from Acl2; only File-backed
  variants were actually called from the repo manager. Removes
  javax.jcr.Node / PathNotFoundException / RepositoryException /
  jackrabbit-commons JcrUtils dependencies from the class.
- Drop javax.jcr:jcr and org.apache.jackrabbit:jackrabbit-jcr-commons
  from the BOM and saiku-service pom.

Fat JAR builds cleanly, REST interface verified end-to-end
(GET /info 200, session login 200, session GET returns admin roles).

FilesystemRepositoryManager/migrate CLI/roles.yml ACL/git-sync
deferred to later iterations per the phase-2 plan's risk-ordered
shipping plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ager

Honest name for the manager that has always been filesystem-backed.
Also drops the never-wired ClassPathResourceDatasourceManager
(its only reference was a commented-out bean in saiku-beans.xml).

Renames:
- class ClassPathRepositoryManager -> FilesystemRepositoryManager
- static accessor getClassPathRepositoryManager -> getInstance
- delete ClassPathResourceDatasourceManager.java
- remove stale classpathDsManager XML comment block

Deferred Phase 2 tasks (plan 2026-04-19-phase-2-filesystem-repo.md):
- 2.4 `saiku migrate jcr-to-fs` CLI: no Jackrabbit left to migrate from
  (purged in previous commit); skipped pending external demand.
- 2.5 roles.yml ACL: existing JSON-backed Acl2 works; revisit when a
  real multi-role deployment needs it (Phase 7 OIDC).
- 2.6 Optional git-sync: opt-in nice-to-have, no signal yet.

Full `mvn clean package` green; REST verified end-to-end:
GET /info 200, POST /session (admin) 200, GET /session returns
admin roles, GET /admin/datasources 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New saiku-semantic module, a self-contained Java 21 / JUnit 5 slice:

- IR records (CubeDefinition, Dimension, Level, Measure) as immutable
  Jackson-bound Java records.
- CubeYamlParser reads YAML via jackson-dataformat-yaml, validates
  required fields, throws SemanticValidationException with the offending
  field name.
- MondrianCompiler walks the IR and emits deterministic Mondrian
  Schema/Cube XML via XMLStreamWriter (byte-identical output for the
  same input — enables golden-file tests later).
- Sample sales.yml fixture + 5 tests (parse, compile, determinism,
  validation failures).

BOM gains jackson-dataformat-yaml and junit-jupiter 5.11.3. Module
registered in saiku-core aggregator.

Deferred: lint (JDBC introspection), infer, convert mondrian-to-yaml,
hot reload, dbt/Cube importers, DuckDB example adapter, docs cookbook.
Those layer on top of this core IR + compiler and can ship
incrementally.

Verified: mvn -pl saiku-core/saiku-semantic -am test (5/5 green),
full clean package green, fat JAR REST smoke test passes (info, login,
session, admin/datasources all 200).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greenfield scaffold (legacy saiku-ui was deleted in 6bba36f):

- Vite 6 + TypeScript 5.7 (strict) + React 18.
- Design tokens in src/styles/tokens.css: spacing/radius/font/shadow
  scales plus light and dark colour scales. Respects
  prefers-color-scheme and a data-theme override persisted to
  localStorage.
- Login form hits POST /rest/saiku/session, falls back to GET /session
  to read the canonical session JSON. Treats any response without
  username/roles as anonymous (the backend returns a stub session for
  unauthenticated GETs).
- Workspace shell placeholder (Cubes sidebar + main pane) with clear
  next-slice markers for AG Grid pivot, Monaco editor, ECharts.
- Vite dev server proxies /rest/* to SAIKU_API (default
  http://localhost:8080). npm run build: tsc --noEmit + vite build,
  148 kB JS bundle (47 kB gzip).
- README documents the dev flow.

Verified end-to-end in Chrome against a live launcher (admin/admin ->
Workspace with roles [ROLE_ADMIN, ROLE_USER]).

Deferred: AG Grid pivot, Monaco, ECharts, Playwright, frontend-maven-plugin
integration (so mvn package bundles dist/ into saiku-webapp).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original Phase 4 (Vite/TS overlay on Backbone) and Phase 6
(SvelteKit rewrite) collapse into a single feature-complete SvelteKit
port now that the legacy Backbone tree has already been deleted from
the working tree.

Changes:

- Restore the legacy Backbone UI as `saiku-ui-legacy/` from commit
  4fa2020^ as a read-only porting reference (399 files, 41 views,
  24 modals, 16 models, 11 plugins). Deleted after the inventory
  hits 100%.
- Wipe the React scaffold from 019d23d.
- Scaffold `saiku-ui/` as SvelteKit 2 + Svelte 5 (runes) + TypeScript
  strict with @sveltejs/adapter-static. `npm run check` passes
  (0 errors, 0 warnings, 267 files).
- Design tokens in `$lib/styles/tokens.css` — light/dark via
  prefers-color-scheme + data-theme override persisted to
  localStorage.
- Session + theme stores using Svelte 5 runes
  ($state/$effect/$effect.root).
- Reusable `Modal.svelte` primitive (backdrop + escape + snippets for
  body and footer, sizes sm/md/lg/xl).
- LoginForm + Workspace shell + inline AboutModal ported. Login +
  session + logout + theme toggle + modal open/close all verified
  end-to-end in Chrome against a live launcher.
- `docs/plans/2026-04-19-phase-4-sveltekit-port.md`: view-by-view
  inventory with ✅/❌ status and 12-slice ordering through full
  feature parity.

Task #26 (Phase 6 SvelteKit rewrite) deleted — merged into this Phase 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports legacy shape: cube <select> dropdown (not tree), measure/dimension
panels under it, workspace toolbar above the query canvas with the
action set from legacy WorkspaceToolbar.js + QueryToolbar.js, Columns /
Rows / Filter drop zones, grid placeholder.

- $lib/api/discover.ts: typed wrappers over GET /rest/saiku/{user}/
  discover[/{connection}/{catalog}/{schema}/{cube}/{dimensions,measures}]
  plus member/{member}/children.
- $lib/stores/datasources.svelte.ts: runes-based store with a `loaded`
  one-shot latch so the $effect guard doesn't loop after empty
  responses. metadata(cube) cache keyed by connection/catalog/schema/
  cube signature.
- $lib/stores/selection.svelte.ts: currently-selected cube.
- session.logout() now clears datasources + selection.
- CubePicker.svelte: legacy-style optgroup-per-schema dropdown +
  refresh button + empty state "No cubes available. Add a datasource
  in the admin console."
- DimensionList.svelte: measures panel (grouped) + dimensions panel
  with hierarchy/level children. Levels and measures marked draggable
  (wiring in the query-execution slice).
- WorkspaceToolbar.svelte: legacy action set — New / Open / Save /
  Save As / Run / Autorun / Non-empty / Swap / MDX / XLS / CSV / PDF.
  Handlers log; hooked up to services in later slices.
- QueryCanvas.svelte: Columns / Rows / Filter dropzones + grid
  placeholder that switches text based on cube selection.

Chrome-verified against the live launcher (no datasources attached):
login → dropdown resolves to "Select a cube", empty-state message
visible, toolbar + dropzones render in legacy layout. Metadata fetch
and actual population still need real datasources + query execution
in the next slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New reusable pieces:
- ConfirmModal.svelte — generic confirm with default/danger variants.
- Modal ports: WarningModal, OverwriteModal, AddFolderModal,
  DeleteRepositoryObject, MoveRepositoryObject, ReportTitlesModal,
  SessionErrorModal, SaveQueryModal, OpenDialogModal. Each cites its
  legacy view source in a header comment.
- ToastStack.svelte + toasts runes store (info/success/warning/danger
  with ttl + manual dismiss).
- repository REST client + runes store (list, get/save/delete/move
  resource, flat + folders helpers).

WorkspaceToolbar now opens real modals instead of console.log:
- New — confirm-discard when dirty
- Open — loads repository tree, opens OpenDialogModal
- Save / Save As — opens SaveQueryModal preseeded with folders
- Run — opens WarningModal ("wires in next slice")
- Swap / MDX / XLS / CSV / PDF — toast placeholders

Chrome-verified against live launcher (no datasources): Save opens the
SaveQuery modal with the folder dropdown + name preset; Run opens the
warning modal; Cancel dismisses. Toast stack renders for Swap/MDX/Export.

Known svelte-check warnings (4, non-blocking): prop-capture
suggestions in SaveQueryModal/MoveRepositoryObject/ReportTitlesModal
— harmless given the $effect(open) resync pattern used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- $lib/api/query.ts — ThinQuery / ThinQueryModel / ThinAxis /
  ThinHierarchy / ThinMeasure types, newQuery() factory matching the
  legacy SaikuOlapQueryTemplate shape, executeQuery() POST to
  /rest/saiku/api/query/execute, cancelQuery().
- $lib/stores/query.svelte.ts — runes-backed query state:
  initFor(cube), includeLevel(axis, drop) moving hierarchies between
  axes, removeHierarchy, addMeasure / removeMeasure, swapAxes,
  setNonEmpty, run() with hasRunnableShape guard.
- DimensionList draggable rows now set
  application/x-saiku-level and application/x-saiku-measure MIME
  payloads on dragstart.
- QueryCanvas replaces the static dropzones with real ondragover /
  ondrop handlers that route payloads to query.includeLevel /
  query.addMeasure. Placed hierarchies + measures render as
  dismissible chips.
- CellsetTable.svelte renders QueryResult.cellset as a sticky-header
  HTML table (row-header + column-header + data-cell styling).
  AG Grid swap-in happens in a later slice.
- WorkspaceToolbar: Run now calls query.run() (falls back to a warning
  modal when no cube is selected). Swap calls query.swapAxes() and
  re-runs if Autorun is on. Non-empty toggle syncs onto ROWS and
  COLUMNS nonEmpty flags. XLS/CSV/PDF buttons open
  /rest/saiku/api/query/{name}/export/{format} in a new tab.

Type-check clean (0 errors; 4 advisory warnings about prop-capture
patterns in modals, harmless due to $effect(open) resync).

This slice cannot be fully Chrome-verified without a real OLAP
datasource — the fat JAR launcher boots with no datasources attached
by default. The shape + API wiring match the server contract; running
against an attached Mondrian datasource will exercise the full path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each modal carries a header comment citing its legacy view source
under saiku-ui-legacy/js/saiku/views/. All use the Modal primitive and
share the design tokens.

Added:
- MDXModal — textarea-backed MDX view/run (Monaco swap in a later slice).
- CalculatedMemberModal — name / parent hierarchy / formula / format.
- FilterModal — custom Order/Filter/TopCount/BottomCount/Limit MDX
  with ASC/BASC/DESC/BDESC sort on Order.
- CustomFilterModal — measure filter by comparator + single or
  BETWEEN value pair.
- DateFilterModal — YEAR/QUARTER/MONTH/WEEK to-date, LAST_N_*,
  CUSTOM_RANGE with date pickers.
- MeasuresModal — search + checkbox picker with calc badge.
- SelectionsModal — include/exclude members for a level with search,
  select-all / clear, member descriptions, open-date-filter hook.
- ParentMemberSelectorModal — searchable member list.
- DrillthroughModal — dimension + measure checkboxes + maxRows with
  Run and Export CSV actions.
- DrillAcrossModal — target cube picker.
- GrowthModal — compare against previous / first / specific member.
- FormatAsPercentageModal — base axis + all/selected scope.
- PermissionsModal — PRIVATE / SECURED / PUBLIC + per-role
  READ/WRITE/GRANT grid.
- DataSourcesModal — connection/catalog/schema/cube table + refresh.

These are UI shells wired against typed props + onSave/onApply/onRun
hooks. Query-state and member-list integration lands per-modal in
subsequent slices when the pieces they depend on exist (e.g.
SelectionsModal needs live member fetching via /discover/cube/member/
children, which comes with drag-drop axis context).

svelte-check 0 errors, 17 advisory warnings (prop-capture pattern,
harmless given $effect(open) resync).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/admin SvelteKit route with tabbed Users / Datasources / Schemas /
Logs panes. Admin-gated (isadmin check, fallback LoginForm or
forbidden banner).

- $lib/api/admin.ts — typed wrappers over /rest/saiku/admin/{users,
  datasources,schema,log/{name},version}, full CRUD + refresh.
- UsersAdmin — list + modal create/edit with username/email/password
  fields and ROLE_USER / ROLE_ADMIN role toggles + delete with
  confirmation.
- DatasourcesAdmin — list + modal create/edit of name/driver/location/
  type/schema/username/password + refresh + delete with confirmation.
- SchemasAdmin — list + XML file/textarea upload modal + delete with
  confirmation.
- LogsAdmin — log-name picker + fetch + monospace viewer.
- Topbar gains Admin and Workspace nav buttons for admin users.

Chrome-verified: login → /admin renders the tabbed shell with a
graceful "users → 500" error callout (the launcher's H2-backed user
DAO is not seeded in the fresh-home case), + Add user button opens
the user modal, tab nav is active. The admin REST endpoints
themselves behave like the legacy UI (same API surface) so once the
deployment has a populated users DB and real datasources, the panes
populate.

svelte-check 0 errors; advisory warnings remain at 18 (prop-capture
pattern, harmless).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Install ag-grid-community ^33 and echarts ^5.
- $lib/views/cellsetUtils.ts: parseCellset(QueryResult) → headerRowCount,
  rowHeaderColCount, row/column categories, per-body-row splits.
  toNumber(cell) tolerant to saiku's raw-prop / comma formatting.
- CellsetTable.svelte rewired on top of AG Grid createGrid() with
  pinned row-header columns, sticky column-header rows, numeric
  right-align + tabular-nums for data cells, sort + filter enabled.
  Grid is re-optioned on every QueryResult change via setGridOption.
  Dark theme via legacy grid theme + local CSS overrides pulling
  design tokens.
- ChartView.svelte: ECharts wrapper rendering ten chart types matching
  the legacy CCC surface:
    bar, stackedBar, line, stackedLine, area, stackedArea,
    pie, heatmap, radar, scatter
  Dark palette tuned to tokens. ResizeObserver keeps the chart sized
  to its host.
- chartTypes.ts: ChartType enum + CHART_TYPES picker catalogue
  (separate module so QueryCanvas can import the list without pulling
  Svelte component exports).
- QueryCanvas gains a Grid / Chart view toggle and a chart-type <select>
  when Chart is active.
- Delete saiku-ui-legacy/js/saiku/plugins/CCC_Chart and CCC_Chart2
  (proto-vis / pvc / tipsy bundles). CCC is not ported; ECharts
  replaces its surface.

svelte-check 0 errors, 19 advisory warnings (prop-capture patterns,
unchanged).

Chrome-verified: login, workspace renders with the new view-toggle
wired in, no console errors. Grid/Chart toggle and each ECharts
type will render against a live Mondrian datasource once one is
attached — the launcher ships without foodmart h2 data in the bundle
so the runtime end-to-end path isn't exercisable from this tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Save flow:
- query store gains savedPath + loadFromJson(raw, path) + markSaved(path).
- Toolbar Save posts ThinQuery JSON to POST /api/repository/resource
  via saveResource(path, content). Re-save on known savedPath skips the
  modal; Save As always reprompts.
- SaveQueryModal defaults the folder + name from the current savedPath
  and appends .saiku to the file name if the user didn't type one.
  Repository tree refreshes after a successful save so the Open dialog
  picks up the new file immediately.

Open flow:
- Toolbar Open loads /api/repository (saiku type filter) if not
  cached, presents OpenDialogModal. Selecting a file GETs
  /api/repository/resource?file=..., feeds the JSON into
  query.loadFromJson(), triggers a re-run when Autorun is on.

MDX:
- Toolbar MDX button opens MDXModal seeded with the current query.mdx
  (if MDX-typed) or the server-enriched mdx on the last result.
- Run MDX switches the query type to MDX, stores the text, and calls
  query.run() against /api/query/execute.

Chrome-verified: login → Open loads the real repository tree (folders
datasources / etc / theme / homes / legacyreports populate from the
live launcher backend), filter + empty state render. Save round-trip
against a populated datasource remains a backend concern (server
currently 500s on /resource without an attached datasource — confirmed
via curl; unrelated to the UI change).

svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gins

- discover.ts: listLevelMembers() + listRootMembers() wrappers.
- QueryCanvas chips now split into a label button (opens
  SelectionsModal for that axis/hierarchy/level) and a separate
  remove (×) button. Member list hydrates from
  /discover/.../levels/{level}, falling back to
  /hierarchies/{h}/rootmembers on error; toast on final failure.
- Selections save is wired at the UI level; persistence into the
  ThinQueryModel's per-level `selection` field lands when the query
  IR exposes the accessor (current shape supports it but the helper
  isn't public yet).
- platform.svelte.ts store: fullscreen-change listener driving the
  topbar toggle, loadVersion() against /admin/version, anonymous
  info ping on mount (legacy plugins/Statistics equivalent).
- Topbar gains a fullscreen toggle button.
- UpgradeBanner.svelte renders a warning strip when
  platform.newVersionAvailable is true (version comparison is a
  follow-up; dismissal persisted to localStorage).

svelte-check 0 errors.

Verified the surface locally: rendered via `npm run build` — bundle
clean. Live SelectionsModal member fetch exercises the /discover
path; against a launcher without attached datasources the fetch
returns 404/500 and the toast + fallback fire as designed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- $lib/i18n/{en,de}.json bundles: 70 keyed strings covering login,
  topbar, cubes panel, dimension/measure panels, workspace toolbar,
  query canvas prompts, view toggle, modal verbs, admin tabs, toast
  messages, and the upgrade banner.
- $lib/stores/i18n.svelte.ts runes store: locale persisted to
  localStorage, initial pick from navigator.language, applies
  <html lang> on change, t(key, fallback) lookup.
- LocalePicker.svelte drops into the topbar next to the theme toggle;
  legacy parity for the ChangeLocale plugin without touching the
  global bean factory model.
- Migrated visible strings in LoginForm, CubePicker, WorkspaceToolbar,
  and the layout topbar to t(). Remaining modals + admin panes still
  carry English literals (sweep layered in follow-up slices).

Chrome-verified: picking "Deutsch" swaps the login form, the topbar
labels, and the <html lang> attribute live without a reload. English
↔ German round-trip preserves all bindings.

svelte-check 0 errors, 19 advisory warnings (prop-capture pattern,
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Install monaco-editor. $lib/monaco/worker-env.ts registers the base
  editor worker via Vite's ?worker import (no JSON/TS/CSS language
  services — we don't need them for MDX).
- $lib/monaco/mdx-lang.ts registers a Monarch MDX tokenizer with ~50
  OLAP keywords (SELECT, FROM, ON COLUMNS, CROSSJOIN, NON EMPTY,
  TOPCOUNT, HIERARCHIZE, CURRENTMEMBER, ...) + bracket-matched
  identifier tokens for [Dim].[Level] references.
- MonacoEditor.svelte wrapper: dynamic-imports monaco only on mount
  (keeps the initial bundle lean), tracks data-theme for vs / vs-dark
  swap on theme toggle.
- MDXModal drops the textarea and renders MonacoEditor when open.
  Strings routed through i18n.
- Tour.svelte: 4-step interactive walkthrough highlighting
  #cubes-select → sidebar → dropzones → view toggle. Dismissal
  persisted to localStorage; restart() exported for future "Replay
  tour" action.
- String sweep continues: OpenDialogModal, SaveQueryModal,
  Workspace/AboutModal, QueryCanvas labels + view toggle + prompts +
  empty states now resolve through i18n.t().

svelte-check 0 errors. Monaco splits into its own chunk (verified by
build output).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…co, i18n sweep

- saiku-webapp pom.xml: wire frontend-maven-plugin to
  install-node-and-npm (v22.11), run npm ci + npm run build in
  saiku-ui/ during generate-resources, then maven-war-plugin
  webResources copies saiku-ui/dist to /ui/ inside saiku.war. After
  this slice `mvn package` produces a WAR that serves the new
  SvelteKit UI at /ui/ alongside the JAX-RS /rest/saiku/* endpoints.
- Selection persistence lands in the query store:
  setLevelSelection(hierarchy, level, members[], type) writes
  level.selection = { type, members:[{uniqueName}] } into the
  ThinHierarchy, and getLevelSelection() reads it back. Clearing
  selections deletes the field (matches legacy behaviour).
- SelectionsModal is now fully wired: opening a chip loads existing
  members from the query state, saving writes back through
  setLevelSelection, optional auto-rerun when query has a runnable
  shape.
- FilterModal + CalculatedMemberModal swap textareas for MonacoEditor
  (mdx language). Autoloaded only when the modal is open.
- String sweep continues through WarningModal + AddFolderModal +
  FilterModal + CalculatedMemberModal footers.

svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…modal

- Add saiku-ui/src/lib/i18n/es.json and fr.json (~70 keys each,
  mirroring en / de). LOCALES picker now lists English, Deutsch,
  Español, Français; locale detector accepts any of the four
  from navigator.language and localStorage.
- $lib/api/http.ts: installAuthInterceptor() wraps globalThis.fetch
  so any /rest/saiku/* response with 401/403 (except /session itself)
  notifies registered listeners. Installed on layout mount.
- Layout now listens for auth failure and pops SessionErrorModal with
  a Reload button. Matches legacy SessionErrorModal.js behaviour.

svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- saiku-webapp pom passes SAIKU_BASE_PATH=/ui to npm run build so
  SvelteKit emits asset URLs rooted at /ui/; adapter-static's
  relative paths mode keeps links portable.
- svelte.config.js reads SAIKU_BASE_PATH env var (defaults to empty
  for local npm run dev on :5173).
- webapp index.jsp now renders as a minimal dark splash that
  immediately redirects to ui/ (instead of the stale Pentaho
  serverdocs link).
- applicationContext-saiku.xml adds `<security:http pattern="..." security="none">`
  chains for /ui/**, /, /index.jsp, /style.css, /images/** so the
  static SPA assets load without Basic-auth pre-prompting the user.
- SaikuLauncher prints Workspace + Admin + REST URLs on boot.
- saiku-ui/.gitignore drops the frontend-maven-plugin-managed .node/
  directory.

Verified end-to-end: `mvn -pl saiku-launcher -am package` produces a
self-contained fat JAR. Launching it:
  Workspace : http://0.0.0.0:8095/ui/
  Admin     : http://0.0.0.0:8095/ui/admin
  REST API  : http://0.0.0.0:8095/rest/saiku/
Chrome loads /ui/ → renders SvelteKit login page (tokens, dark mode,
Sign-in form); /rest/saiku/session 200s on admin/admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- i18n bundles gain 19 keys covering admin tabs' add / edit / delete /
  refresh / empty state / per-user column headers (username, email,
  roles, password), LogsAdmin fetch/loading/idle strings, and modal
  titles for Warning / Delete / Overwrite / AddFolder / SessionError
  + Reload button.
- UsersAdmin, DatasourcesAdmin, SchemasAdmin, LogsAdmin + /admin page
  tabs + forbidden banner now resolve via i18n.t().
- WarningModal, AddFolderModal, SessionErrorModal titles + session
  reload button migrated.

svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy Backbone/gulp/jQuery UI tree (~380 files, 41 views, 24
modals, 16 models, 11 plugins) has been ported to SvelteKit across
slices 1 through 15. Retaining it as a read-only reference is no
longer useful — the history is in git (commit 4fa2020^ has the
last intact copy) and the porting inventory doc cross-references
every view that landed in the new tree.

Summary of what the new tree covers:
- Shell: login, topbar, fullscreen, theme, locale, upgrade banner,
  session-error interceptor, tour walkthrough.
- Workspace: cube picker + measure/dimension panels with live fetch,
  drag-drop to Columns/Rows/Filter axes, chip-click member selections
  with persistence into ThinQueryModel, swap axes, non-empty, autorun.
- Query execution: POST /rest/saiku/api/query/execute, cellset
  rendering via AG Grid Community, ECharts chart view with ten chart
  types (bar/stacked bar/line/stacked line/area/stacked area/pie/
  heatmap/radar/scatter), Grid/Chart view toggle.
- Modals: About, SaveQuery, OpenDialog, Warning, Confirm, Overwrite,
  AddFolder, Delete, Move, ReportTitles, SessionError, MDX (Monaco),
  Calculated member (Monaco), Filter (Monaco), CustomFilter, DateFilter,
  Measures, Selections (with live member fetch), ParentMember,
  Drillthrough, DrillAcross, Growth, FormatAsPercentage, Permissions,
  DataSources.
- Repository: list, get, save, delete, move resources; query
  save/open round-trip through /api/repository.
- Export: XLS / CSV / PDF wired to /rest/saiku/api/query/{name}/export.
- Admin: Users / Datasources / Schemas / Logs with full CRUD + modals.
- i18n: en, de, es, fr bundles; LocalePicker with navigator
  auto-detect + localStorage persistence; <html lang> sync.
- Deployment: frontend-maven-plugin runs `npm ci && npm run build`
  during `mvn package`; maven-war-plugin bundles dist/ into the WAR
  at /ui/; saiku-launcher fat JAR serves it at
  http://host:port/ui/ and prints Workspace + Admin + REST URLs on
  boot.

Deliberately not ported:
- BIServer plugin (Pentaho integration — consumed only by the
  p7.1 plugin module which was deleted in 51ecff0 / 4fa2020).
- CCC_Chart / CCC_Chart2 plugins — replaced wholesale by ECharts.
- intro.js, qtip, tipsy, spectrum, fancybox, notify, ozpIwc vendor
  bundles — superseded by the Tour + Toast + Modal primitives and
  native browser UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Admin/Workspace topbar <a href> hardcoded "/" which escaped the /ui mount
  on direct click, producing 403 from Jetty's root servlet. Use {base} so
  hrefs resolve to /ui/admin and /ui/ when deployed under SAIKU_BASE_PATH.
- adapter-static was only emitting dist/index.html; direct navigation to
  /ui/admin/ returned 404. Enable prerender + trailingSlash:"always" so
  dist/admin/index.html is generated for hard loads and deep-links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three regressions were hiding the foodmart cube from the workspace:

1. JdbcUserDAO loaded database-queries.properties from the classpath, but
   the file only lived under WEB-INF/; nothing staged a classpath copy
   after the Jackrabbit removal. Admin /users returned 500 ("SQL must
   not be null"). Restore a classpath-root copy under
   saiku-webapp/src/main/resources.

2. SaikuLauncher only mkdir'd saiku-home/data; it never staged the
   bundled FoodMart4.xml or foodmart_h2.sql script. loadFoodmart
   swallowed the resulting FileNotFoundException and the datasource
   list stayed empty. Bundle both assets under
   saiku-launcher/src/main/resources/seed/ and extract/unzip on boot.

3. commons-vfs2 2.10 transitively needs httpclient 4.3+ (TrustStrategy);
   BOM pinned httpcore 4.3-alpha1 + httpclient 4.2.5 so Mondrian
   ApacheVfs2VirtualFileHandler crashed on every schema URL. Bump to
   httpclient 4.5.14 + httpcore 4.4.16.

Also replace mondrian:///datasources/foodmart4.xml with a plain
file:// URL pointing at the staged FoodMart4.xml, since the custom
MondrianVFS provider was deleted in Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- UI now sends ThinHierarchy.name as the hierarchy unique name (matching
  backend convention in Thin.java) and drops the non-existent
  `uniqueName` field that was failing Jackson deserialization with
  "Unrecognized field" on /rest/saiku/api/query/execute.
- LevelDrop payload adds dimensionName so ThinHierarchy.dimension can
  be populated from drag-drop.
- Guard against query.getAxis(PAGES) returning null in Fat.convertAxis
  so the PAGES axis (sent empty by the new UI) no longer NPEs at
  qaxis.setNonEmpty().
- Workspace layout/viewport + jersey tracing fix carry-overs from the
  same testing pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rips out @ag-grid-community (v35 theming API kept fighting the layout —
2px root wrapper nested inside a 538px host, plus no nested row
semantics for OLAP cellsets) in favour of a Svelte-native HTML table
that mirrors legacy Saiku's rendering and interactions:

- Renders parseCellset output as a semantic <table> with th.row /
  th.row_null / th.col / td.data classes matching the legacy
  SaikuTableRenderer markup.
- Nested row headers: repeated parent values collapse into
  row_null cells (Mondrian's empty-string convention + uniquename
  fallback for older cellsets) so USA/CA appear once across their
  children.
- Sticky column header (thead) and sticky pinned first-column
  row headers for horizontal scrolling.
- Custom right-click context menu on row/column header cells with
  Keep Only / Include Level ▸ / Remove Level ▸ / Filter Level… —
  drills the query store, disables already-used levels, and emits a
  saiku-filter-level CustomEvent that QueryCanvas wires to the
  SelectionsModal.
- cellsetUtils.rowHeaderDisplay() computes the is-null/merged state
  per row-header cell up front, keeping the template simple.

Removes @finos/perspective* deps and vite tweaks that were briefly
trialled — WASM bundle + shadow-DOM theming didn't give us nested
expand/collapse or a usable custom context menu without writing a
bespoke plugin, so the pragmatic call was a thin native table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts autorun state into the query store and routes every mutator
(include/remove level, add/remove measure, set selection, swap axes,
non-empty, setMeasures) through a markDirty() helper that fires the
query when autorun is on — previously the toolbar owned local autorun
state so removing a chip via the × button set dirty=true but never
triggered a re-run.

Adds sidebar affordances that mirror the legacy view:
- Measures panel header gets a gear (bulk toggle via MeasuresModal)
  and a + button (opens the Calculated Measure wizard). Saving the
  wizard appends to queryModel.calculatedMeasures and adds the new
  [Measures].[<name>] member to the visible measure list.
- Per-level gear (visible on hover) opens SelectionsModal pre-
  targeted to that hierarchy/level so filters can be set without
  dragging to an axis first.

Turns the calculated-measure dialog into a proper wizard with
name/parent/format fields, a preview of the generated MDX, and three
palettes (measures to insert, operator chips, function templates with
caret positions) that insert at the current textarea cursor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy Saiku treated every axis chip the same — tap the label to edit
member selections, click the × to remove. The rewrite had wired that
split for COLUMNS/ROWS but the FILTER drop-zone was still a single
button that removed on click, making members impossible to filter via
that axis.

Split the FILTER chip into the same label + × pattern so clicking the
label routes through openSelections("FILTER", hier).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buggtb and others added 5 commits April 19, 2026 16:41
- New chart types: donut, treemap, sunburst, bubble (sized scatter),
  waterfall (transparent-spacer stacked bar). CHART_TYPES now carries a
  `group` so the picker renders as grouped optgroups (Bars / Lines /
  Proportional / Matrix / Points).
- ChartOptions (title, axis labels, legend on/off + position, trend
  line) threaded through ChartView with a new ChartEditorModal opened
  via the ⚙ button on the chart view toolbar. Trend line supports
  linear regression, moving average and weighted moving average with a
  configurable period; overlay on the first measure of line/stacked
  line/area charts.
- Rebuilt WorkspaceToolbar into grouped buttons + Tools and Export
  dropdowns so the export triplet and the rarely-used tools live
  behind proper menus. Tools ▾ now wires MDX, Drill across
  (DrillAcrossModal populated from datasources.connections) and
  Report titles (persists into queryModel.properties).
- Cellset data cells now fire saiku-drillthrough on right-click;
  QueryCanvas listens and opens DrillthroughModal (dimensions /
  measures / max rows). Run goes through the new drillthrough API
  helper and renders the result in a new DrillthroughResultModal.
  Export CSV opens the backend's export path. Backend currently
  throws a classloader error on the endpoint under the fat-jar
  launcher (see memory); UI round-trip is correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mondrian's mondrian.rolap.SqlStatement#getWrappedResultSet() calls
Proxy.newProxyInstance(null, new Class<?>[]{ResultSet.class}, …) which
fails on JDK 17+ with "java.sql.ResultSet referenced from a method is
not visible from class loader: null" because java.sql moved into its
own named module and can no longer be resolved through the bootstrap
loader.

Shadow the class inside saiku-webapp/src/main/java so Servlet-spec
loader precedence (WEB-INF/classes wins over WEB-INF/lib) pulls in the
patched copy. Only the loader argument changes — we pick the thread
context classloader and fall back to ResultSet.class.getClassLoader()
then ClassLoader.getSystemClassLoader().

Also sets saiku-webapp's maven-compiler-plugin to the parent pom's
source/target (21) so the shadowed source actually compiles; the
parent default of 1.6 is no longer accepted by modern javac.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Installs lucide-svelte and swaps the emoji/text icons across the
workspace toolbar, the left sidebar panel actions, and the
dimension/measure tree for consistent stroke-style SVG glyphs.

- Toolbar: icon-only File buttons (new / open / save / save-as),
  primary Run button with Play icon + label, Swap icon, Tools menu
  (wrench), Export menu (download). Dropdown items get their own
  icons (MDX braces, Drill-across arrows, Report tags, XLS/CSV/PDF
  file-type icons).
- Sidebar: Settings2 gear + Plus for the Measures header, ChevronDown/
  Right for every tree twisty, Sigma for measure rows (coloured like
  the legacy UI), Folder for dimensions, GitFork for hierarchy nodes,
  Minus for leaf levels, Filter for the per-level gear (fades in on
  hover).
- i18n strings stripped of inline emoji so the SVG is the icon and
  the label stays clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sparklines + split Run button with autorun/non-empty dropdown

- Right-click a measure chip on the COLUMNS drop-zone for a context
  menu (CustomFilterModal for "Filter by value…", FormatAsPercentage
  for "Format as percentage…", GrowthModal for "Growth calculation…",
  plus Remove). Format % and Growth write a calculated measure into
  queryModel.calculatedMeasures and add the resulting member to the
  visible measures list. Filter by value wraps the ROWS axis in a
  FILTER() expression.
- Right-click a hierarchy chip for Edit-selections + Remove.
- Each axis drop-zone grows a "⋯" action button (visible on hover)
  that opens FilterModal pre-configured with the expression type —
  Order/Filter/TopCount/BottomCount/Limit — and writes the MDX onto
  the axis (or sortOrder / sortEvaluationLiteral for Order).
- New view-toggle buttons: Grid · Chart · Stats · Sparkline · Sparkbar.
  StatsView computes Count/Min/Max/Sum/Avg/StdDev per data column.
  SparklineView renders per-row SVG sparklines or sparkbars with
  first/last/min/max summary columns.
- Reworked the Run button into a split control: primary Run on the
  left, chevron caret on the right that drops down with Autorun and
  Non-empty as menu items. Click-outside collapses the run menu.
- New lightweight ContextMenu component in $lib/components reused by
  measure/hierarchy/axis menus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sparklines: sparkline/sparkbar are no longer a separate view. CellsetTable
now takes an optional \`spark\` prop ("line" | "bar") and appends a
Sparkline/Sparkbar column to the existing grid — every body row gets a
tiny inline SVG chart built from that row's data cells, matching the
legacy saiku-ui behaviour. The orphan SparklineView.svelte is removed.

Measure-chip + per-axis MDX: the first pass wrote Axis(0).Item(0)…
expressions which Mondrian rejects inside WITH MEMBER and FILTER. All
four handlers now dereference the primary ROWS/COLUMNS hierarchy
directly:

- Filter by value: FILTER(<primary rows level>.Members, measure op value)
- Format as percentage: IIF(([m], <hier>.CurrentMember.Parent) = 0, null,
  [m] / ([m], <hier>.CurrentMember.Parent)) with ROWS / COLUMNS /
  GRAND_TOTAL scopes. Warns when the chosen axis has no hierarchy.
- Growth calculation: (m - (m, <rowsHier>.CurrentMember.PrevMember)) /
  prev with a null-guard. "first" uses FirstSibling, "specific" accepts
  a user-supplied member unique name.
- Per-axis FilterModal (Order / Filter / TopCount / BottomCount /
  Limit): builds the base set from the axis hierarchies' deepest level
  and CROSSJOINs when multiple. Pre-seeds the expression field with a
  per-type placeholder (e.g. "10, [Measures].[Unit Sales]" for
  TopCount) instead of leaking the previous axis.mdx into a new type.

Verified end-to-end in-session: Filter-by-value narrowed 3 states to 2,
Format-as-% emitted CA=28.02% / WA=46.62%, Growth rendered
WA=83.81% (vs prev. member CA), and TopCount on State Province sorted
the three rows by Unit Sales DESC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant