feat: governance tags - #372
Conversation
Adds a Tags tab to warehouse/namespace/table/view/generic-table pages (table page also gets column tags), a new /governance route hosting the Tag Definition manager, and a Governance nav item. Depends on @lakekeeper/console-components governance-tags release; dep bump pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a centralized Permissions tab to the Governance page (gated to OpenFGA backends), and standardize main page titles (Identities, Governance, Warehouses, Server settings) on a text-h6 heading with pa-4 padding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate the tab on the authz backend being OpenFGA instead of server-level grant-read, so project admins (who lack server read_assignments) still see it; per-scope access is handled inside the explorer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Namespace page: add Details tab (last, default stays namespaces) rendering NamespaceDetails; remove inline tags card; tag management via cog menu. Entity pages (table/view/generic-table): thread can-manage-tags; tags now live in Details/cog per the console-components redesign. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the tag detail page rendering TagDetail from console-components. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tab bar's URL-sync watcher called router.replace() on every single tab click, which runs Vue Router's full navigation-guard pipeline — including an awaited getServerInfo() network call — on every switch. That async work was interrupting the v-tabs-window slide transition (confirmed via its lifecycle hooks: leave completed, enter never fired). Sync the tab into the URL via window.history.replaceState() instead, which updates the address bar without running navigation guards; route.query.tab is only ever read once on mount, so a reactive route update isn't needed. Also give the namespace page the same pinned header+tabs layout (flex column content, v-card as the scrolling region) that table/view/warehouse already had, so it behaves consistently and the v-tabs-window has a stable height to animate within. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A directional left/right slide implies the tabs are a spatial sequence, which doesn't hold for independent facets of the same object (Details/Permissions/ Health/etc). Add Vuetify's built-in crossfade prop to every v-tabs-window that switches between independent views, matching the pattern used by professional dense-data tools. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Interim pin to lakekeeper/console-components@014d152 (its feat/governance-tags PR, not yet released) so this branch builds against the real feature instead of the local npm-link copy. Re-pin to the released version tag once that PR merges and release-please cuts it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 13 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
WalkthroughThe console updates its components reference, adds Governance pages and typed routes, and revises administrative and warehouse layouts. Warehouse tab changes now update bookmarkable URLs through browser history without router navigation guards. ChangesGovernance and navigation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant NavigationBar
participant Router
participant GovernancePage
participant OpenFGA
NavigationBar->>Router: Navigate to /governance
Router->>GovernancePage: Render governance route
GovernancePage->>OpenFGA: Read authorization backend
OpenFGA-->>GovernancePage: Return backend type
GovernancePage-->>Router: Synchronize selected tab with URL query
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/pages/governance/index.vue`:
- Around line 35-42: Update the tab initialization and synchronization around
tab and showPermissionsTab so only tags, or permissions when showPermissionsTab
is true, can remain selected; normalize invalid or unavailable query values to
tags. Watch route.query.tab and showPermissionsTab, update the local tab
accordingly, and preserve the existing router query synchronization without
leaving the page with an unrendered tab.
In `@src/pages/warehouse/`[id].namespace.[nsid].table.[tid].vue:
- Line 92: Update the tab content rendered inside the v-tabs-window associated
with tab so previously visited v-tabs-window-item bodies remain mounted during
crossfade. Replace current-tab-only v-if rendering with v-show or an equivalent
visited-tab cache, preserving the existing tab content and transitions.
In `@src/pages/warehouse/`[id].namespace.[nsid].view.[vid].vue:
- Line 89: Update the tab body rendering under v-tabs-window so the previous tab
component remains mounted during crossfade transitions; replace the current-tab
v-if with v-show, or otherwise cache visited tab bodies, while preserving the
existing active-tab behavior.
In `@src/pages/warehouse/`[id].namespace.[nsid].vue:
- Around line 358-364: Keep Vue Router’s current query.tab synchronized while
preserving the lightweight tab URL update and avoiding the full navigation-guard
pipeline. Update the tab watcher/helper at
src/pages/warehouse/[id].namespace.[nsid].vue lines 358-364,
src/pages/warehouse/[id].namespace.[nsid].table.[tid].vue lines 344-350, and
src/pages/warehouse/[id].namespace.[nsid].view.[vid].vue lines 309-315
consistently, using a router-safe helper or tab-only guard bypass, and verify
Back/Forward navigation retains the selected tab.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a81a4de5-62bd-415d-93d3-41e6d81e4309
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (15)
package.jsonsrc/assets/dependencies.jsonsrc/auto-imports.d.tssrc/components/NavigationBar.vuesrc/pages/governance/index.vuesrc/pages/governance/tags/[id].vuesrc/pages/identities.vuesrc/pages/server-settings.vuesrc/pages/warehouse/[id].namespace.[nsid].generic-table.[tid].vuesrc/pages/warehouse/[id].namespace.[nsid].table.[tid].vuesrc/pages/warehouse/[id].namespace.[nsid].view.[vid].vuesrc/pages/warehouse/[id].namespace.[nsid].vuesrc/pages/warehouse/[id].vuesrc/pages/warehouse/index.vuesrc/typed-router.d.ts
| const tab = ref((route.query.tab as string) || 'tags'); | ||
|
|
||
| // Permission assignments are an OpenFGA concept — show the tab whenever the | ||
| // backend is OpenFGA (per-scope visibility is handled inside the explorer). | ||
| // Cedar surfaces policies instead; allow-all has no permission management. | ||
| const showPermissionsTab = computed(() => visual.getServerInfo()['authz-backend'] === 'openfga'); | ||
|
|
||
| watch(tab, (t) => router.replace({ query: { ...route.query, tab: t } })); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Normalize and synchronize the selected tab.
Line 35 accepts any tab query value. ?tab=permissions on a non-OpenFGA backend, or ?tab=other, selects no rendered v-tabs-window-item and leaves the page blank. Normalize the query value to tags unless Permissions is available and selected. Also update tab when the route query or showPermissionsTab changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/governance/index.vue` around lines 35 - 42, Update the tab
initialization and synchronization around tab and showPermissionsTab so only
tags, or permissions when showPermissionsTab is true, can remain selected;
normalize invalid or unavailable query values to tags. Watch route.query.tab and
showPermissionsTab, update the local tab accordingly, and preserve the existing
router query synchronization without leaving the page with an unrendered tab.
| // Update the URL bar without going through router.replace(): that runs the full | ||
| // navigation-guard pipeline (including an awaited getServerInfo() network call) | ||
| // on every tab click, which was stalling/interrupting this page's tab transition. | ||
| // route.query.tab is only ever read once on mount, so a reactive route update | ||
| // isn't needed here — just keep the URL bookmarkable/shareable. | ||
| const href = router.resolve({ query: { ...route.query, tab: newTab } }).href; | ||
| window.history.replaceState(window.history.state, '', href); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
files=(
'src/pages/warehouse/[id].namespace.[nsid].vue'
'src/pages/warehouse/[id].namespace.[nsid].table.[tid].vue'
'src/pages/warehouse/[id].namespace.[nsid].view.[vid].vue'
)
for file in "${files[@]}"; do
printf '\n== %s ==\n' "$file"
rg -n -C 5 'window\.history\.replaceState|router\.(push|replace)|watch\(tab' "$file"
doneRepository: lakekeeper/console
Length of output: 7615
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '== package deps ==\n'
if [ -f package.json ]; then
node -e 'const p=require("./package.json"); console.log({ "vue-router": p.dependencies?.["vue-router"] || p.devDependencies?.["vue-router"], vuecli: p.dependencies?.["`@vue/cli-service`"] || p.devDependencies?.["`@vue/cli-service`"] });'
fi
printf '\n== route/tab guards and getServerInfo ==\n'
rg -n "getServerInfo|beforeEach|beforeResolve|tab|history\.state|replaceState|pushState" src -S
printf '\n== navigation files ==\n'
git ls-files 'src/**/*.vue' | rg 'warehouse/\[id\].namespace' | sed -n '1,40p'Repository: lakekeeper/console
Length of output: 36171
🌐 Web query:
Vue Router 5.0.4 history.replaceState update history state current location source html5 pushState state
💡 Result:
Vue Router 5.0.4, released on March 19, 2026, continues to manage the browser's history state internally to track navigation context, such as scroll position and navigation direction [1][2][3]. When using HTML5 history mode, the router internally utilizes the window.history.pushState and window.history.replaceState APIs [4][5]. Developers interacting with history states should be aware of the following technical details: 1. Internal State Management: Vue Router relies on window.history.state to store its own metadata [6]. If you manually invoke window.history.replaceState or window.history.pushState, you risk overwriting this internal data [7][6]. 2. Preserving State: To modify the history state without breaking Vue Router's functionality, you must merge your custom state with the existing state [7][6]. The recommended pattern for replacing state is: history.replaceState({...history.state,...newState }, '') [6] 3. Router API Integration: Vue Router provides internal mechanisms to update history entries. While developers can use router.push and router.replace, they are designed to manage navigation. When custom state persistence is required alongside navigation, developers often combine router navigation with manual history state adjustments or rely on the router's internal state handling [8][9]. 4. Security and Browser Quirks: Vue Router wraps the browser's history methods in a try-catch block [5]. If an error occurs (such as Safari's security limitation on frequent state updates), the router may fallback to location.replace or location.assign to ensure the navigation completes [5]. Version 5.0.4 focuses on bug fixes—such as resolving iterator helper compatibility for Node 20 and fixing issues with named views and auto routes—and does not introduce changes to the core HTML5 history state handling logic [2][10].
Citations:
- 1: https://github.com/vuejs/router/releases/tag/v5.0.4
- 2: https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md
- 3: vuejs/router@7f32e99
- 4: https://router.vuejs.org/guide/essentials/navigation.html
- 5: https://github.com/vuejs/router/blob/ea8e3e21/packages/router/src/history/html5.ts
- 6: https://router.vuejs.org/guide/migration/
- 7: Allow a user to read and write state on history entries (history.state) vuejs/vue-router#2243
- 8: Allow changing `history.state` with `router.push()` vuejs/rfcs#400
- 9: https://router.vuejs.org/api/interfaces/routerhistory
- 10: https://npmx.dev/package-changelog/vue-router/v/5.0.4
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re, urllib.request as u
router_html5 = Path('/tmp/vue-router-5.0.4-h5.ts')
url = 'https://raw.githubusercontent.com/vuejs/router/v5.0.4/packages/router/src/history/html5.ts'
u.urlretrieve(url, router_html5)
text = router_html5.read_text()
for name in ['_setHistoryStateWhenAvailable', '_updateCurrentLocation', 'replaceState']:
idx = text.find(name)
print(f'\n== {name} around index {idx} ==')
print(text[max(0, idx-900):idx+1400])
print('\n== replaceState implementation references ==')
for m in re.finditer(r'(\n\s*replaceState[\s\S]{0,800}?\n\s*(?:_setHistoryStateWhenAvailable|\n\s*else)\n)', text):
print(m.group(1))
PY
printf '\n== router.beforeEach excerpt ==\n'
sed -n '60,155p' src/router/index.tsRepository: lakekeeper/console
Length of output: 2571
🌐 Web query:
Vue Router history state replaceState currentLocation router.push replace state.history.state source
💡 Result:
In Vue Router, navigation methods like router.push and router.replace are designed to mirror the native browser History API, specifically window.history.pushState and window.history.replaceState [1][2]. Key Concepts regarding History State and Navigation: Navigation Methods: router.push adds a new entry to the browser's history stack, while router.replace replaces the current history entry [1][2]. You can also perform a replacement by passing { replace: true } within the navigation object to router.push [1][2]. Managing History State: Vue Router internally utilizes the browser's history.state to persist information such as scroll position and navigation metadata [3][4]. Because the router relies on this object, direct manual manipulation of history.state can cause conflicts [3]. Adding Custom State: If you need to attach custom data to a history entry, you can pass a state property to router.push or router.replace [5][4]. Example usage: router.push({ path: '/home', state: { myCustomKey: 'value' } }) Updating State After Navigation: If you need to modify the history state after a navigation has already occurred, you can use the native history.replaceState API to merge new data with the existing state, which preserves internal router data [3][6]. Example: await router.push('/new-path') history.replaceState({...history.state, myCustomKey: 'newValue' }, '') Important Considerations: - Serializability: Any state passed must be serializable (e.g., no functions, Symbols, or non-plain objects) [5][4]. - Preservation: When using native history.replaceState, always spread existing state (history.state) to avoid overwriting internal router data, such as scroll position or key tracking [3][6]. - Consistency: These navigation methods work consistently across different router history modes (e.g., HTML5 History, Hash) [2][7].
Citations:
- 1: https://github.com/vuejs/router/blob/main/packages/docs/guide/essentials/navigation.md
- 2: https://router.vuejs.org/guide/essentials/navigation.html
- 3: https://router.vuejs.org/guide/migration/
- 4: Allow changing `history.state` with `router.push()` vuejs/rfcs#400
- 5: https://github.com/vuejs/router/blob/main/packages/router/src/types/index.ts
- 6: Allow a user to read and write state on history entries (history.state) vuejs/vue-router#2243
- 7: https://v3.router.vuejs.org/guide/essentials/navigation
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import ssl, urllib.request as u, re
ctx = ssl._create_unverified_context()
router_html5 = Path('/tmp/vue-router-5.0.4-h5.ts')
url = 'https://raw.githubusercontent.com/vuejs/router/v5.0.4/packages/router/src/history/html5.ts'
u.urlretrieve(url, router_html5)
text = router_html5.read_text()
for symbol in ['currentLocation', 'replaceState', '_setHistoryStateWhenAvailable', '_updateCurrentLocation']:
idx = text.find(symbol)
print(f'\n== {symbol} ==')
if idx == -1:
continue
print(text[max(0, idx-800):idx+1800])
print('\n== deterministic excerpt around replaceState/currentLocation usages ==')
matches = list(re.finditer(r'\n\s*(?:function|const|async)?\s*(?:replaceState|currentLocation|_updateCurrentLocation|_setHistoryStateWhenAvailable)\s*[\(:{]', text))
for mm in matches:
print(f'{text[mm.start():mm.start()+120].strip()}')
PY
printf '\n== router guard relevant section ==\n'
sed -n '1,130p' src/router/index.tsRepository: lakekeeper/console
Length of output: 2571
🌐 Web query:
Vue Router html5.ts replaceState call _setHistoryStateWhenAvailable currentLocation.value
💡 Result:
In Vue Router (specifically the Vue Router 4 html5.ts history implementation), replace is a method that updates the browser's history entry without adding a new one [1][2]. When replace is called, it performs the following sequence of operations: 1. State Construction: It creates a new state object by merging the existing history.state with a new buildState object (which preserves navigation details like forward/back pointers and the current position) and any additional user-provided data [1]. 2. changeLocation Execution: It calls changeLocation(to, state, true) [1]. - Inside changeLocation, the browser's history.replaceState(state, '', url) is executed [1]. - A try/catch block wraps this call to handle browser quirks (such as SecurityError exceptions) by falling back to location.replace(url) if necessary [1][3]. - The internal historyState.value is updated to reflect the new state [1]. 3. currentLocation Update: After the browser history is successfully updated, the currentLocation.value is updated to the new destination [1]. While _setHistoryStateWhenAvailable does not exist as a standard method in current Vue Router 4 source code, internal state management relies on tracking historyState.value (a Ref or reactive container) to ensure Vue Router's navigation system remains in sync with the actual window history [1][4]. This mechanism ensures that data like scroll positions and navigation direction are correctly maintained across push and replace operations [1][5].
Citations:
- 1: https://github.com/vuejs/router/blob/ea8e3e21/packages/router/src/history/html5.ts
- 2: https://github.com/vuejs/router/blob/ea8e3e21/packages/router/src/history/common.ts
- 3: createWebHistory() initial replaceState throws SecurityError when document URL contains userinfo (HTTP basic-auth in URL) vuejs/router#2714
- 4: Allow changing `history.state` with `router.push()` vuejs/rfcs#400
- 5: https://github.com/vuejs/router/blob/main/packages/router/src/router.ts
Keep query.tab in sync with Vue Router when updating the URL.
watch(tab, ...) changes the URL with history.replaceState, but Vue Router’s current location can still carry the previous tab during later navigation to the same page. For the affected warehouse pages, use a router-safe helper or a tab-only guard bypass and test Back/Forward behavior.
src/pages/warehouse/[id].namespace.[nsid].vue#L358-L364src/pages/warehouse/[id].namespace.[nsid].table.[tid].vue#L344-L350src/pages/warehouse/[id].namespace.[nsid].view.[vid].vue#L309-L315
📍 Affects 3 files
src/pages/warehouse/[id].namespace.[nsid].vue#L358-L364(this comment)src/pages/warehouse/[id].namespace.[nsid].table.[tid].vue#L344-L350src/pages/warehouse/[id].namespace.[nsid].view.[vid].vue#L309-L315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/warehouse/`[id].namespace.[nsid].vue around lines 358 - 364, Keep
Vue Router’s current query.tab synchronized while preserving the lightweight tab
URL update and avoiding the full navigation-guard pipeline. Update the tab
watcher/helper at src/pages/warehouse/[id].namespace.[nsid].vue lines 358-364,
src/pages/warehouse/[id].namespace.[nsid].table.[tid].vue lines 344-350, and
src/pages/warehouse/[id].namespace.[nsid].view.[vid].vue lines 309-315
consistently, using a router-safe helper or tab-only guard bypass, and verify
Back/Forward navigation retains the selected tab.
Was pinned to the feat/governance-tags PR's commit SHA as an interim measure before that PR released; point at the real tag now that it has. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Governance page: fall back to the Tags tab if ?tab=permissions is selected but the Permissions tab isn't actually available (non-OpenFGA backend, or landing before serverInfo has loaded) — previously left the page with no tab body rendered at all. - Table/view detail pages: once a tab is visited, keep its content mounted (Vuetify's own window-item toggling handles show/hide) instead of unmounting it the instant you switch away. The immediate v-if swap meant the outgoing tab was already blank by the time the crossfade transition started, so it faded from nothing rather than from real content. Skipped (already deliberately designed, re-verified): the tab-sync watcher across namespace/table/view pages intentionally bypasses router.replace via history.replaceState (that's what fixed the original tab-switch stutter — router.replace runs the full navigation-guard pipeline, including an awaited getServerInfo() call, on every click). route.query.tab is read once on mount, nothing else depends on it reactively, and this was grepped/confirmed earlier in the same work. No new information here changes that. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # package-lock.json
Summary
@lakekeeper/console-componentsgovernance tags feature into the app: Identities gains a Tags tab (project-scoped tag vocabulary), warehouse/namespace/table/view pages gain entity tag management via a "Manage tags" cog action, and a routed tag detail page at/governance/tags/:id.router.replace()(runs the full navigation-guard pipeline, including an awaitedgetServerInfo()) on every click — switched tohistory.replaceState().v-tabs-windowgroups (independent facets, not a wizard sequence) now usecrossfadeinstead of a directional slide.chore(ui)commit). Needs re-pinning to the released version tag once that PR merges and release-please cuts it.Test plan
just reviewable(format + lint + build) passesnpx vue-tsc --noEmitpasses afternpm run unlinkconsole-e2e(specs/flows/governance-tags.spec.ts) — companion, verified passing against a from-source backend build; blocked on a published backend image with the tag-authorization actions before it can run in CIBEGIN_COMMIT_OVERRIDE
feat(ui): governance tags — Tags tab on Identities, entity tag management, tag detail route
feat(ui): governance permissions tab + consistent page titles
fix(ui): tab-switch animation stalled by router.replace's navigation-guard pipeline; use history.replaceState
fix(ui): facet-style tab groups use crossfade instead of a directional slide
chore(ui): bump console-components to the governance-tags branch commit (interim, pending release)
END_COMMIT_OVERRIDE
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements