PR 1 review: every comment and what I'll do

rancher/ui-configurable-views PR 1, reviewed by richard-cox on 2026-11-03 and 04 at 0f34877. Page written 2026-11-05.

Preview: base preview, built from 0f34877 on a shell we packed in October, which predates the new stock cards. Backend mf Rancher (v2.16.0-7758e3e-head). The bar starts hidden: press Ctrl/Cmd+Shift+V, or use the list icon in the header. Baseline: the shared dashboard-master preview, built from 261b8932f, the current tip.

Where it stands: nothing has changed on the PR yet

No code, no replies. Of Richard's 32 threads, 23 get fixed in this PR, 1 in a follow-up PR, 3 get a reply, 1 waits on a shell release, 2 wait on UX and 2 need your call. The review body adds 3 more fixes (M1-M3). Later and TBD items stay out.

What changed since the plan I sent you

All comments

#Richard's commentWhat I'll doEffortStatus
MVP items from the review body
M1The 3-second delay on a fresh instanceReview bodyRead once, no retriesSmallFix in this PR
M2Differences from the stock cluster dashboardReview bodyFive differences, each with a fixMediumFix in this PR
M3Ctrl+Shift+V clashes with pasteReview bodyChange it to Shift+VSmallFix in this PR
Cleanup and metadata
1Old extension name in gen-manifestsscripts/gen-manifests.mjs:3Delete the scriptSmallFix in this PR
2Is gen-manifests still needed?scripts/gen-manifests.mjs:1No. Goes with thread 1SmallFix in this PR
3Is crs-to-configmaps still needed?scripts/crs-to-configmaps.mjs:1No. Delete the scriptSmallFix in this PR
4Root package namepackage.json:2Rename to ui-configurable-viewsSmallFix in this PR
5Prime-only flagpackage.json:6Add the annotationSmallFix in this PR
6Bump the Rancher version to 2.16package.json:8Set >= 2.16.0SmallFix in this PR
7Does anything use these redirects?routing/index.ts:4No. Delete the file and its constantsSmallFix in this PR
8Orphaned commenttemplating/widget-catalog.ts:271Delete itSmallFix in this PR
9Could use clusterUrltemplating/widget-data.ts:115Use it here and at line 67SmallFix in this PR
10Overlap between two fetch functionstemplating/widget-data.ts:103fetchClusterRows calls fetchClusterPageSmallFix in this PR
11Use the shell's CONFIG_MAPtemplating/template-engine.ts:21Import itSmallFix in this PR
12VuexStore instead of Storetemplating/template-engine.ts:19Swap the 24 referencesSmallFix in this PR
13Constants for the page keystemplating/stock-layouts.ts:86Add two constants next to PageKeySmallFix in this PR
14Should ViewSet be named after Page?templating/types.ts:160Rename to PageViewsMediumFix in this PR
15Use the shell's Schema typetemplating/resource-types.ts:5Extend it locally for nowSmallFix in this PR
16Comments above functions should be JSDoccomposables/useEditSession.ts:119Convert all 55MediumFix in this PR
17The hardcoded list and SSPtemplating/view-model.ts:303Remove the list, explain SSP in a replySmallFix in this PR
Behaviour fixes
18Detail links on table rowscomponents/widgets/WidgetTable.vue:86Bring links back, pointing at the widget's clusterMediumFix in this PR
19Does this cache survive log out?templating/widget-data.ts:154Key it by user, drop expired entriesSmallFix in this PR
20Input text not vertically centredcomponents/ViewBar.vue:5181px of bottom padding on both input classesSmallFix in this PR
Component swaps
21RcButton instead of buttons with classescomponents/EditViewSidebar.vue:510Convert 29 of the 30 raw buttonsMediumFix in this PR
22Most buttons can be RcButtonscomponents/ViewBar.vue:547Same work as thread 21SmallFix in this PR
23Use Tabbed and Tabcomponents/widgets/WidgetTabs.vue:92Switch, keeping open-on-hover while draggingMediumFix in this PR
Follow-up PR
24Flip widget settings into the widgetscomponents/WidgetSettingsModal.vue:442Own PR right after this oneLarge
Reply only
25Why does a downstream CRD table work?templating/widget-data.ts:50Explain itSmallReply only
26Pagination context is homecomponents/widgets/WidgetTable.vue:195Confirm, explain the two pathsSmallReply only
27canList already does thistemplating/resource-types.ts:30Explain, and match its conditionsSmallReply only
Waits on a shell change
28isObject belongs in the shelltemplating/view-model.ts:135Keep it local until a shell releaseSmallWaits on shell
Waiting on a decision or UX
29Where the on/off switch livestemplating/template-engine.ts:158Decision 1: remove the stored switchSmallYour call
30Make clear tables don't updatetemplating/widget-data.ts:112Decision 2: propose, then buildMediumYour call
31UX: discuss these pagesproduct.ts:15No code until UX answersNone yetWaits on UX
32Should bar visibility be a preference?composables/useViewBarVisibility.ts:3No code until UX answersNone yetWaits on UX

Four decisions for you

Everything else can start without these. To answer, something like "1A 2A 3A 4A, skip thread 23" is enough.

1. Should I remove the stored kill switch? thread 29

Today it's a value in a ConfigMap that non-admins can't read, and they read "can't tell" as on. So turning it off doesn't reach the people it's meant for.

2. Should I build the "not live" hint and Refresh button now? thread 30

Richard lists this as MVP but tags it for UX and dev to discuss. Here's what I'd propose. It's a mock-up drawn into the page, with nothing behind it.

Mock-up: "Not live: read at 14:02" and a Refresh button on a widget that reads once.
Mock-up: "Not live: read at 14:02" and a Refresh button on a widget that reads once.

3. Should this repo become the only place the code changes?

These fixes make the new repo differ from configurable-home and configurable-home-demo on rancher-ai-template, and from configurable-views on your dashboard fork.

4. When do we fix storage for non-admins? New

Richard's QA list expects non-admin users to manage views on both dashboards. Views live in ConfigMaps in default on the local cluster, and those users can't touch them. Only admins and local cluster owners can.

User on mfRead viewsCreateChange
Standard user, cluster member on created-dashboard-2 Richard's first personaForbiddenForbiddenForbidden
Standard user, cluster member on localForbiddenForbiddenForbidden
Standard user, cluster owner on localAllowedAllowedAllowed
User-base, project owner on created-dashboard-2 More access than Richard's second persona, a project memberForbiddenForbiddenForbidden
User-base, project roles on localForbiddenForbiddenForbidden
User-base, project owner on local and created-dashboard-2ForbiddenForbiddenForbidden

Real requests with kubectl --as, and --dry-run=server for the writes, so nothing was written.

What they'd see: only the Stock Dashboard tab. They can build a whole view, but Save fails, so there's no way to keep it. I simulated this in an admin session with the configmap schema removed. It's not a real non-admin login.

The Save error with no access to ConfigMaps.
The Save error with no access to ConfigMaps.

Draft comment on the review

One for your QA list: the non-admin scenarios can't pass yet. Views are in ConfigMaps in default, and both personas get Forbidden on read and write, so they only see the stock tab and Save fails. I'd treat your TBD storage items as the next PR after this one: personal views in user preferences, published views in a CRD like the table views. Does that work, or do you want it in this PR?

MVP items from the review body

These have no thread of their own. "Make extension prime only" is thread 5.

M1

The 3-second delay on a fresh instance

SmallFix in this PR
MVP: fresh instance, no views, there's a noticable (+3 second) delay when navigating to home/explorer dashboard page and seeing the dashboard content. think this is caused by 5 sequental requests to /v1/configmaps/default?pagesize=100000&filter=metadata.labels[templates.rancher.io/ai-templating]%20IN%20(true)&exclude=metadata.managedFields. i could not reproduce this later on when there were views

What I found

Reproduced, and Richard's diagnosis is right. useViewSet.load() keeps retrying, 500ms apart, up to 5 reads, whenever it finds nothing. Stubbed reads answer instantly, so the 2 seconds below are only the waiting. Real reads add their own time, which fits Richard's 3+.

It's also wider than a fresh install. Anyone who can't read the ConfigMaps never finds anything, so they wait those 2 seconds on every visit to the Home or a cluster dashboard.

CaseHome shows afterDashboard shows afterReads
Stock master, no extension31-133ms47-249ms0
Extension, views stored (real data)215-437ms211-260ms2
Extension, nothing stored (stub, Richard's case)2,050-2,122ms2,064-2,116ms7-10
Extension, read answers 403 (stub)2,046-2,127ms2,070-2,094ms6
Extension, no configmap schema (stub)2,049-2,115ms2,046-2,061ms0

3 in-app navigations per case, on the base preview and the master preview. The 2-second wait with 0 reads is the retry loop waiting for a read it skips.

What I'll do

The loop's comment gives two reasons for retrying: the user may not be known yet, and the ConfigMaps may not exist yet. Asking for the user first solves the first. The second went away with the seeding code. So: get the user, read once, and share that read with the onEnter hook, which is why even stored views make 2 reads today. A failed read leaves the stock page showing, and the next navigation reads again.

This changes my plan. I said "retry only if the read failed", but a 403 is a failed read, so non-admins would still wait. No retry at all is simpler and fixes both.

To check it, I'll re-run this table, and open the Home straight after login to make sure views still appear.

M2

Differences from the stock cluster dashboard

MediumFix in this PR
MVP: Delta between new Cluster Explorer Dashboard components and stock Cluster Explorer Dashboard components (they should use the same components, with the table widget wrapping the dashboard one)

What I found

I told you this was the resource cards. The cards are the biggest part, but comparing them side by side shows five differences. Same cluster and window size. The numbers on the shots match the table under them.

Stock: master preview at 261b8932f.
Stock: master preview at 261b8932f.
Ours: the Rebuilt dashboard view on the base preview, on the older shell.
Ours: the Rebuilt dashboard view on the base preview, on the older shell.
#StockOursWhyFix
1Status cards: Nodes, Deployments, unhealthy workloadsCount cards: 855 Total Resources, Nodes, DeploymentsStock moved to ResourceStatusCards on 2026-10-17. Our widget always draws ResourceSummaryRender ResourceStatusCards on the open cluster, as Events and Alerts already do. Needs shell 3.0.15-rc.4. Other clusters keep today's cards, because the stock ones only read the cluster store
2Thin bars, grey headingThick bars, black headingBars: only our preview's older shell. The widget already uses the stock HardwareResourceGauge, so a rebuild brings them. Heading: the widget has its own <h3>, and stock added text-deemphasizedCopy the heading's classes
3Has an Alerts tab hereNo Alerts tab hereStock checks for any app in cattle-monitoring-system. We look for the monitoring chart by name. This cluster has only rancher-monitoring-crd and no monitoring pods, so stock shows a tab with nothing behind itUse stock's check, from the cluster's counts
4No row above the Events filterExtra "Explorer (2) / Add View" rowStock's page does provide: { showTableViewTabs: false }. Ours doesn'tProvide it from the widgets that draw the dashboard's tables
5Cluster Tools about 20px from the pinAbout 10px from the pinWe redraw the header, because stock has it inline in the pageCopy the spacing

What I'll do

Fix all five, bump the root @rancher/shell to 3.0.15-rc.4 (an RC until 3.0.15 ships), and rebuild the previews on it. Then I'll compare again on the same shell and list anything left, including the Alerts and metrics tabs on a cluster that really has monitoring.

What this won't fix: the cluster header and component status stay redrawn, because stock has them inline in its page. Truly sharing them needs a shell PR that pulls them out. I'll ask Richard whether they want that.

M3

Ctrl+Shift+V clashes with paste

SmallFix in this PR
MVP: Hide views bar (with tabs) shortcut is currently ctrl + shift + v which clashes with unformatted paste
The bar this shortcut shows and hides.
The bar this shortcut shows and hides.

What I found

Shift plus a letter is already how the shell does this: its page templates bind Shift+T for the theme and Shift+L for the locale, and detail pages and tables use C, D, E, Y, J and K. V isn't taken, in the shell or in this package. The shell's shortcut plugin stays out of the way while focus is in an input, textarea, select or editor, so typing a capital V in a form is safe.

What I'll do

Set the shortcut to ['shift', 'v'] on both platforms and update the comment at index.ts:63-66. I'll press it on the preview, on a page and inside a text field.

The 32 threads

Two ways a table reads

Threads 17, 25 and 26 are all about this, and it's why Richard saw no SSP.

From a cluster (fromCluster: true)From Rancher (fromCluster: false)
Drawn byResourceTable, rows from useClusterPagePaginatedResourceTable, context home
RowsUp to 500, read once. The browser pages them, and a note says when the list was cutLocal cluster and Rancher types, through the store
SSPNeverOnly clusters: home turns it on for CAPI.RANCHER_CLUSTER and MANAGEMENT.CLUSTER alone
SchemaThe store if it knows the type, else that cluster's /v1/schemasThe store
Cleanup and metadata

First commit. Mechanical, nothing a user would see.

1

Old extension name in gen-manifests

SmallFix in this PR
few references to the old extension name here
The code it points at
1#!/usr/bin/env node
2/* eslint-disable no-console */
3// Regenerate pkg/ai-templating/install/manifests.ts from the YAML sources, so the extension can
4// embed and auto-install its cluster dependencies (see pkg/ai-templating/install/ensure.ts).
5//
6//   node scripts/gen-manifests.mjs

What I found

It regenerates pkg/ai-templating/install/manifests.ts. That folder no longer exists, and nothing in the repo calls the script.

What I'll do

Delete scripts/gen-manifests.mjs.

2

Is gen-manifests still needed?

SmallFix in this PR
is this still needed?
The code it points at
1#!/usr/bin/env node
2/* eslint-disable no-console */
3// Regenerate pkg/ai-templating/install/manifests.ts from the YAML sources, so the extension can
4// embed and auto-install its cluster dependencies (see pkg/ai-templating/install/ensure.ts).

What I found

No, for the same reason as thread 1.

What I'll do

Same delete as thread 1.

3

Is crs-to-configmaps still needed?

SmallFix in this PR
same with this one, still needed?
The code it points at
1#!/usr/bin/env node
2/* eslint-disable no-console */
3// One-shot: convert the CustomView/HomeTemplate example CRs into labeled ConfigMaps (the new
4// storage). Overwrites the example files in place.

What I found

It was a one-shot that converted old example CRs into ConfigMaps. Those examples are gone, and nothing calls it.

What I'll do

Delete scripts/crs-to-configmaps.mjs. That empties scripts/.

4

Root package name

SmallFix in this PR
think this is called out in template, but creating review comment just for tracking. name needs updating
The code it points at
1{
2  "name": "rancher-ai-template",
3  "version": "0.1.0",
4  "private": false,
5  "engines": {

What I found

Still rancher-ai-template, left over from the exact copy. Once the scripts go, the only other old name is the ai-templating label and its test. That label stays on purpose, because it is on every stored ConfigMap.

What I'll do

Set name to ui-configurable-views. The extension's own package stays configurable-views.

5

Prime-only flag

SmallFix in this PR
the prime only flag goes in here somewhere
The code it points at
3  "description": "configurable-views plugin",
4  "version": "0.1.0",
5  "private": false,
6  "rancher": {
7    "annotations": {
8      "catalog.cattle.io/rancher-version": ">= 2.10.0",
9      "catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"

What I found

The shell reads catalog.cattle.io/prime-only and refuses the extension on a non-Prime Rancher (plugins.error.primeOnly). The shell's helm patch script copies rancher.annotations from this file into the chart.

What I'll do

Add "catalog.cattle.io/prime-only": "true" under rancher.annotations. This also closes the "Make extension prime only" MVP. Baked-in previews skip this check, so only a real install proves it.

6

Bump the Rancher version to 2.16

SmallFix in this PR
we can bump this up to 2.16
The code it points at
5  "private": false,
6  "rancher": {
7    "annotations": {
8      "catalog.cattle.io/rancher-version": ">= 2.10.0",
9      "catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
10    }
11  },

What I found

It says >= 2.10.0. The shell turns any version with a dash into x.y.999 before comparing, so 2.16 RCs and head builds pass >= 2.16.0.

What I'll do

Set catalog.cattle.io/rancher-version to >= 2.16.0.

7

Does anything use these redirects?

SmallFix in this PR
not sure i get this. we're redirecting from c-cluster-"configurable-views"-x to "configurableviews"-c-cluster-x? does anything use c-cluster-configurable-views?
The code it points at
1import type { RouteRecordRaw } from 'vue-router';
2import { ROUTE_SETTINGS, ROUTE_VIEWS } from '../templating/template-engine';
3 
4// The product's own pages are routed by the Product API (see product.ts). These are only where they
5// used to be, so an old link or bookmark still lands. They sit under the 'default' layout - named, as
6// the shell asks of an extension's routes.
7const LAYOUT = 'default';

What I found

Nothing does. They send the old prototype paths (/c/:cluster/configurable-views, /views, /layouts) to the product's pages, which the Product API routes by itself. ROUTE_SETTINGS and ROUTE_VIEWS are only used here.

What I'll do

Delete routing/index.ts, the plugin.addRoutes(routing) call and the two constants. An old prototype bookmark stops working, which is fine for something unreleased.

Draft reply

Nothing uses them. They were redirects for old prototype bookmarks, so I removed them.

8 SmallFix in this PR
orphaned comment?
The code it points at
268  return list.filter((entry) => textOf(entry).toLowerCase().includes(needle));
269}
270 
271/**
272 * The resources offered in a widget's "Resource" picker. Any type Rancher knows can be typed in,
273 * but these are the ones worth suggesting.
274 */

What I found

Yes. It describes a Resource picker list that no longer exists, and sits right above the doc block of PAGINATION_CONTEXT.

What I'll do

Delete lines 271-274.

9 SmallFix in this PR
could use clusterUrl
The code it points at
112  const res = await store.dispatch('management/findPage', {
113    type: resource,
114    opt:  {
115      url:        `/k8s/clusters/${ encodeURIComponent(cluster) }/v1/${ resource }`,
116      transient:  true,
117      watch:      false,
118      pagination: whole ? {

What I found

clusterUrl() is at line 173 of the same file. fetchClusterPage (115) and fetchClusterSchemas (67) both build that URL by hand.

What I'll do

Use clusterUrl() in both.

10

Overlap between two fetch functions

SmallFix in this PR
there's overlap between fetchClusterPage and fetchClusterPage, can one use the other?
The code it points at
100  serverPaged: boolean;
101}
102 
103export async function fetchClusterPage(store: Store<unknown>, {
104  resource, cluster, page = 1, pageSize = 10, sort = [], whole = false, cap = 500
105}: ClusterPageArgs): Promise<ClusterPage> {
106  if (!resource || !cluster) {

What I found

The comment names fetchClusterPage twice. I read the second as fetchClusterRows (line 359), which makes the same findPage call and only adds filters.

What I'll do

Give fetchClusterPage an optional filters argument. fetchClusterRows then calls it with whole: true and returns rows and truncated.

11

Use the shell's CONFIG_MAP

SmallFix in this PR
we have this one already - shell/config/types.d.ts CONFIG_MAP
The code it points at
18 
19type Getters = Store<unknown>['getters'];
20 
21const CONFIGMAP = 'configmap';
22const NAMESPACE = 'default';
23 
24// Every stored ConfigMap carries the marker; the type label tells them apart.

What I found

CONFIG_MAP in @shell/config/types has the same value.

What I'll do

Drop the local CONFIGMAP and import it.

12

VuexStore instead of Store

SmallFix in this PR
up to you, but for all references to vuex Store you could use shell/types/store/vuex.d.ts VuexStore. That might make it easier if we ever type that store properly
The code it points at
16import { migrateViewSet } from './view-model';
17import type { ViewSet } from './types';
18 
19type Getters = Store<unknown>['getters'];
20 
21const CONFIGMAP = 'configmap';
22const NAMESPACE = 'default';

What I found

The comment says it's optional, but it's cheap. VuexStore is in published shell 3.0.14 and later, and there are 24 non-test references to Store<...>.

What I'll do

Replace them. If the type check finds a place where VuexStore is too narrow, I'll list it rather than cast around it.

13

Constants for the page keys

SmallFix in this PR
can home and if used somewhere cluster dashboard keys be consts somewhere
The code it points at
83 * like the dashboard, the view leaves them out wherever a cluster does not have them.
84 */
85export function stockWidgets(page: PageKey, t: Translate): WidgetNode[] {
86  if (page === 'home') {
87    const { gap, pad } = stockView(page);
88 
89    // The banner runs edge to edge over the view's inset, as the Home's does; the panel below it

What I found

'home' means three things in this package: a page key, a links source and the pagination context. Only the page key and 'clusterDashboard' are what this comment is about.

What I'll do

Add PAGE_HOME and PAGE_CLUSTER_DASHBOARD next to PageKey, and derive the type from them. The other two meanings of 'home' stay. Stored keys don't change.

14

Should ViewSet be named after Page?

MediumFix in this PR
terminology wise should this name contain or be Page? i think it's then something like... - Page - views (which are the page's tabs) - view (a page tab) - widgets - table - overview - tabs - widgets
The code it points at
157export type View = LayoutView | StockView;
158 
159/** What one scope - the organization, or one person - has saved for a page: its views, and which opens first. */
160export interface ViewSet {
161  views: View[];
162  defaultViewId?: string;
163  /**

What I found

It holds what one scope, the organization or one person, saved for a page: its views, which one opens first, and the tab order. Plain Page doesn't fit, because PageConfig already holds all the scopes of a page. The type name isn't stored anywhere.

What I'll do

Rename ViewSet, useViewSet and migrateViewSet to PageViews, usePageViews and migratePageViews. That's 63 references in 12 files, tests included. It's mechanical, so it gets its own commit.

Draft reply

Agreed with your hierarchy. I renamed it to PageViews, since it's one scope's views for a page. Plain Page would clash with PageConfig, which holds all the scopes. Nothing stored changes.

15

Use the shell's Schema type

SmallFix in this PR
can this use shell/plugins/steve/schema.d.ts (would need updating)
The code it points at
2// cluster's. Pure functions - no Vue, no store.
3 
4/** A schema as Steve serves it - the parts the Resource picker reads. */
5export interface TypeSchema {
6  id: string;
7  collectionMethods?: string[];
8  attributes?: {

What I found

Schema in shell/plugins/steve/schema.d.ts has id and attributes, but not collectionMethods, attributes.kind or attributes.group, which the Resource picker reads. Richard already expects it needs updating.

What I'll do

Import Schema and extend it locally with those three fields. The shell type gets them in the same dashboard PR as thread 28, then the local part goes.

Draft reply

Done. It extends Schema locally for the three fields it lacks. I'll add them to the shell type in a dashboard PR and drop the local part then.

16

Comments above functions should be JSDoc

MediumFix in this PR
there's a few dangling comments above functions instead of jsdocs across quite a few files. worth getting ai to do a convert to jsdoc run
The code it points at
116    }
117  }
118 
119  // Write the draft to your account.
120  async function save({ keepEditing = false } = {}): Promise<void> {
121    const draft = working.value;
122 

What I found

A script finds 55 // comments directly above a function, in 21 files. useGridEditing and useEditSession have the most, 7 each. There's also a doc block in the wrong place at widget-data.ts:188.

What I'll do

Convert them to /** */ and move the misplaced one. Section-divider comments stay. Comment-only commit, so it's easy to review.

17

The hardcoded list and SSP

SmallFix in this PR
something feels off here for items in this hardcoded list we basically don't do server-side pagination, given view-models.ts READ_FROM_A_CLUSTER.includes(out.resource)? unless that's set directly to cluster in widgetsettingsmodal? i gave this a test with pods and my CR and in both dashboards we're not doing SSP
The code it points at
300    out.ownViews = true;
301  }
302  if (out.kind === TABLE_KIND) {
303    out.fromCluster = typeof w.fromCluster === 'boolean' ? w.fromCluster : READ_FROM_A_CLUSTER.includes(out.resource);
304  }
305 
306  // `subtitle` and `image` are banner-only extras; keep them only when set so stored specs stay small.

What I found

READ_FROM_A_CLUSTER only fills in fromCluster for tables saved before that setting existed. All 5 tables stored on mf set it explicitly, so nothing depends on it. It also isn't why Richard saw no SSP. See two ways a table reads.

What I'll do

Remove the list, so a table with no fromCluster reads from Rancher, and update the test at view-model.test.ts:151. In PR 2, the agent's prompt has to start setting fromCluster, because it doesn't mention it today.

Draft reply

That list only fills in fromCluster for tables saved before the setting existed. It doesn't choose anything for a new table. Every stored table we have sets it, so I removed it. No SSP in your test is a separate thing: a table reading from a cluster reads up to 500 rows and pages them in the browser, and a table reading from Rancher uses the home context, where only the two cluster types have SSP on. That's your "Later: Integrate SSP".

Behaviour fixes

Second commit, with the MVP fixes above.

18 MediumFix in this PR
You should be able to make use of the LinkDetail getCustomDetailLink to - check for a cluster in the url - swap out the target cluster This means the name will work, but also for other usages like namespace and pod node links
The code it points at
83 * wrong cluster - the stock Home's own cluster table drops it for the same reason.
84 */
85function withoutDetailLink(header: TableColumn): TableColumn {
86  if (header?.formatter !== 'LinkDetail') {
87    return header;
88  }
89 

What I found

Richard is right, and there's a live bug next to it. I put a table of local's pods on created-dashboard-2's dashboard, in my browser only. Name has no link, because withoutDetailLink removes it. Namespace links to /c/c-m-bmsk94st/explorer/namespace/static-preview and Node to /c/c-m-bmsk94st/explorer/node/ip-10-0-0-253, both into the wrong cluster. On the Home those columns are plain text.

The catch: getCustomDetailLink only reaches formatters that declare it. Name uses LinkDetail, which does. Namespace uses LinkName and a pod's Node uses LinkNamePopover. Neither does, and both read the open cluster from the store. A table-level function also gets only the row, so it can't tell columns apart. My plan said this would only miss the node link. It misses the namespace link too.

A and B link into created-dashboard-2, but these rows belong to local. The view only existed in my browser.
A and B link into created-dashboard-2, but these rows belong to local. The view only existed in my browser.

What I'll do

On the open cluster the stock formatters are already right, so they stay. For a table of any other cluster, headersFor swaps each link column for LinkDetail with its own getCustomDetailLink in formatterOpts, which the table binds after its own prop, so it wins. Name uses row.detailLocation, reference columns such as a PVC's claim use their reference, and Namespace and Node build the route from their type and value. All of them get the widget's cluster and the explorer product. Tables reading from Rancher treat local as their cluster. In those tables the Node popover goes away, because it fetches from the open cluster. I'll check pods, PVCs, a CRD and a Rancher type such as clusters on the preview.

Draft reply

You're right, and it's more than a missing link: on a cluster's dashboard, a table of another cluster links Namespace and Node into the open cluster. getCustomDetailLink brings back Name. Namespace and Node use LinkName and LinkNamePopover, which don't take it, so for a table of another cluster I swap those columns to LinkDetail with their own link in formatterOpts.

19

Does this cache survive log out?

SmallFix in this PR
will this persist over log out / in? normally it would be yes, but for extensions do we unload on log out? might be safer as a cache in the core or management stores
The code it points at
151// request answers both: an answer is kept for a few seconds, and a request still in flight is
152// shared rather than repeated. Short on purpose - this is de-duplication, not a cache to go stale.
153const SHARED_MS = 5000;
154const shared = new Map<string, { at: number; promise: Promise<unknown> }>();
155 
156function once<T>(key: string, load: () => Promise<T>): Promise<T> {
157  const hit = shared.get(key);

What I found

It depends on the install. From reading the shell: auth/logout removes non-builtin extensions and login loads the script again, so a real extension starts fresh. Baked-in builds, like our previews, keep the module, so entries can survive for up to their 5-second window. An onLogout hook wouldn't help a real extension, because it's removed before that hook runs. monitoring in useWidgetPresence has the same shape with a 60-second window. I haven't tested this with a real extension.

What I'll do

Add the user's id to the cache keys and delete expired entries on each read, in both places.

Draft reply

Yes for baked-in builds, no for a real extension: logout removes non-builtin plugins and login loads the script again, so module state starts fresh. To be safe in both, I keyed the entries by user and drop expired ones. A store module felt like more than a 5-second de-dupe needs, but I can move it if you prefer.

20

Input text not vertically centred

SmallFix in this PR
not sure i've got the right input, but the text isn't vertically centred
Richard's screenshot: the bar's input with the text scratchay

Richard, a few minutes later:

ah, not here, in pkg/configurable-views/components/EditViewSidebar.vue. the one in view --> drop down --> rename view is fine
The code it points at
515          :data-testid="`configurable-views-tab-${ view.id }`"
516          @mousedown="startTabDrag(view, $event)"
517        >
518          <input
519            v-if="editing && view.id === activeId"
520            ref="nameInput"
521            class="vbar__name"

What I found

My first guess, the missing type="text", was wrong: adding it changes no computed style. Richard's screenshot is the bar's edit input (30px box, text about 1px low), and my render matches it. The rename input Richard calls fine has the same class and measures the same, so I can't reproduce a difference between them. It's the font sitting a little low, not a broken rule.

Input, measured with an HChromiumFirefoxWebKitWith 1px padding
Bar, edit mode (.vbar__name)+0.83px+0.50px+0.17px−0.17px in all three
Bar, rename (.vbar__name)+0.83px+0.50px+0.17pxSame class
Sidebar name (.evs__field)+0.33px+0.67px+0.67px+0.33 / +0.33 / 0px
Stock list filter, for comparison+0.33pxNot measuredNot measured

Positive means below centre. CSS pixels, from a 3x screenshot.

What I'll do

padding-bottom: 1px on .vbar__name and .evs__field. I tried it with an injected style in all three engines, which gave the last column.

Draft reply

I measured these inputs in Chromium, Firefox and WebKit. They all sit up to 1px low, and the edit input and the rename input in the bar measure the same. 1px of bottom padding on both classes brings them within about 0.3px everywhere. If you still see it, which browser are you on?

Component swaps

Third commit. Should look the same as today.

21

RcButton instead of buttons with classes

MediumFix in this PR
i think we use RcButton now everywhere instead of buttons with classes
The code it points at
507        >
508          {{ t('configurableViews.sidebar.publish') }}
509        </button>
510        <button
511          class="btn btn-sm role-link evs__wide evs__danger"
512          @click="$emit('delete')"
513        >

What I found

There are 30 raw <button>s in 7 files. I told you 31, but one was the word in a code comment. Everything outlined red in this shot is a raw button.

Red outlines added in my browser, with a CSS rule on buttons that aren't .rc-button.
Red outlines added in my browser, with a CSS rule on buttons that aren't .rc-button.
GroupCountWhereWhat happens
Already btn role-*15ConfirmDialog 2, EditViewSidebar 3, ViewBar 4, WidgetSettingsModal 3, SavedViews 3Same look expected, with variant and size
Icon-only8Sidebar close, drawer toggle, widget gear and remove, dialog close, tab move and removevariant="ghost", keeping their labels
Chips and pills5Start-from chips 2, width, height and spacing pills 3variant="ghost" plus their class. I'll check the selected state
Bar tab1ViewBar:547See thread 22
Growl action1UndoGrowl:47Stays. It copies the stock growl, which uses the same raw growl-action button

What I'll do

Convert 29, then check each group on the preview in both themes.

22

Most buttons can be RcButtons

SmallFix in this PR
think _most_ (all?) of the buttons can be RcButtons
The code it points at
544            :distance="4"
545            :aria-label="t('configurableViews.bar.moreActions')"
546          >
547            <button
548              :ref="(el) => keepRef(tabButtons, view.id, el as HTMLElement | null)"
549              type="button"
550              role="tab"

What I found

This line is the bar's tab button. It can convert: a role="tab" passed from here overrides RcButton's own role, and RcButton exposes focus() for the arrow keys.

What I'll do

Convert it with thread 21. If the tab's look or keyboard handling suffers, I'll keep this one and say why in the reply.

23 MediumFix in this PR
Should this use the Tabbed and Tab components?
The code it points at
89</script>
90 
91<template>
92  <div class="wtabs">
93    <h3
94      v-if="widget.title"
95      class="wtabs__title"

What I found

The comment in the file gives two reasons for not using Tabbed. The URL hash one is out of date, because it has useHash (in 3.0.14 too). The other is that its tabs are child components, while these are data that change during editing. A v-for of Tab should handle that, but I haven't tried it. The hard part is dragging a widget over a tab to open it, which Tabbed doesn't do. The strip already copies its look, so users shouldn't notice the swap.

What I'll do

Tabbed with :use-hash="false", one Tab per item (name, label, weight, and badge for the count while editing), content behind a v-if so hidden tabs don't fetch, and a drag listener on the strip for open-on-hover. If that breaks dragging, I'll keep the strip and tell you why before replying.

Follow-up PR

Too big for this PR. Starts right after it merges.

24

Flip widget settings into the widgets

Large
can we flip this somehow Widgets contain the metadata to do stuff like showing data source things (which is currently the case) but also supply a vue component to configure whatever settings it needs? that keeps components as owners of what they show and their configuration... and means extensions in the future can easily add widgets
The code it points at
439          </template>
440        </template>
441 
442        <template v-if="draft.kind === 'banner'">
443          <label class="wsm__label">{{ t('configurableViews.widgetSettings.subtitle') }}</label>
444          <input
445            v-model="draft.subtitle"

What I found

Richard is right about the shape. A new widget kind touches three places today: a v-if block in this dialog (lines 342, 382, 406, 442, 456, 475), RENDERERS in WidgetHost.vue and CLUSTER_WIDGETS in widget-catalog.ts. An extension can't add to any of them.

A table's settings.
A table's settings.
A banner's settings. Each is a block in the same file.
A banner's settings. Each is a block in the same file.

What I'll do

Each kind declares its component, its settings component and its metadata in one entry. The dialog keeps only what they all share. It's the biggest change here (the dialog is 760 lines, 950 in the demo), so I'd keep it out of this PR to avoid holding up the base. Tell me if you want it in PR 1 instead.

Draft reply

Agreed, that's the right shape: each kind brings its component, its settings component and its metadata, so extensions can add widgets. It touches the dialog, the host and the catalog, so I'd do it as its own PR right after this one rather than hold up the base.

Reply only

Nothing to change, or almost nothing. Richard asked a question and these answer it.

25

Why does a downstream CRD table work?

SmallReply only
something feels off here i created a CRD in a downstream cluster. i then created a home page list targeting that cluster and using the CRD. on refresh the page correctly loads the table and shows results from that cluster. according to this though it should fail to load given it's not in management, current cluster (cluster) or cluster?
The code it points at
47 
48  const stores = ['management', getters['currentStore'](type), 'cluster'];
49 
50  return stores.find((store) => store && getters[`${ store }/schemaFor`]?.(type)) || 'management';
51}
52 
53/** Every cluster the user can see, as picker options, by the name a person would recognise. */

What I found

storeForType only picks a store to look a schema up in, and falls back to management. For a table reading from a cluster, nothing depends on that: the schema comes from that cluster's /v1/schemas, and the rows from a findPage given that cluster's URL.

What I'll do

No change.

Draft reply

storeForType only picks a store to look the schema up in, and falls back to management. A table reading from a cluster doesn't depend on it: the schema comes from that cluster's /v1/schemas, and the rows from a findPage with that cluster's URL. That's why your CRD loaded.

26 SmallReply only
i'm not 100% sure but this could be one of the reasons not many lists use server-side pagination The pagination context is set to home... which is only set for a couple of the cluster types.
The code it points at
192      :schema="schema"
193      :headers="headers"
194      :pagination-headers="pagedHeaders"
195      :context="PAGINATION_CONTEXT"
196      :override-in-store="inStore"
197      :table-views="true"
198      :table-view-tabs="viewTabs"

What I found

Richard is right. See two ways a table reads.

What I'll do

No change now. This is the "Later: Integrate SSP" item.

Draft reply

You're right, home only has SSP on for the two cluster types, so a table from Rancher of anything else pages in the browser. Tables from a cluster don't use that path at all: they read up to 500 rows and say so when the list is cut. I've left this for your "Later: Integrate SSP".

27

canList already does this

SmallReply only
this is on shell/models/steve-schema.ts canList it doesn't to the get, but it's the common place we check if we can list
The code it points at
27 * Can a list of it be read? A Kubernetes kind whose collection answers a GET, and, where the schema
28 * says which verbs it allows, one of them is `list` - some, like a kubeconfig, can only be made.
29 */
30export function isListable(schema: TypeSchema): boolean {
31  const verbs = schema.attributes?.verbs;
32 
33  return !!schema.attributes?.kind &&

What I found

canList is a getter on the schema model, and its helper canSchema isn't exported. These schemas are raw JSON from another cluster, so they aren't models. canSchema asks for a collection link and the verb. isListable asks for a kind, a GET collection method and the verb only if verbs are present.

What I'll do

Keep isListable, but make it ask what canList asks as well: the collection link, and always the verb. This is the one small code change in this group.

Draft reply

canList is a getter on the schema model, and these schemas are raw JSON from another cluster, so I can't call it. I kept isListable and made it check the same collection link and verb.

Waits on a shell change

Needs a dashboard PR and a shell release, on the 2.17 milestone.

28

isObject belongs in the shell

SmallWaits on shell
feels like one for shell/utils/object.js
The code it points at
132 
133type Loose = Record<string, unknown>;
134 
135function isObject(v: unknown): v is Loose {
136  return !!v && typeof v === 'object' && !Array.isArray(v);
137}
138 

What I found

shell/utils/object.js doesn't export one today. Moving it needs a dashboard PR and a shell release.

What I'll do

Keep it local for now. Do it in one dashboard PR with the Schema fields from thread 15. Two other shell changes would help later: unhide the feature flag, and let useResourceStateSummaries take a cluster.

Draft reply

It isn't exported from there today, and moving it needs a dashboard PR and a shell release, so it stays local for now. I'll do it together with the Schema type change.

Waiting on a decision or UX
29

Where the on/off switch lives

SmallYour call
this will need to live in a place where all users can access it. i think we can shift it to be per user, and in userpreferences. alternatively we can just rely on - global feature flag - temporary runtime disable (if the user then wants to disable feature flag via ui)
The code it points at
155 
156/** data.enabled === 'false' turns the feature off. Absent, or anything else, means on. */
157export function isTemplatingEnabled(getters: Getters): boolean {
158  return cmNamed(getters, CONFIG_NAME)?.data?.enabled !== 'false';
159}
160 
161/** The URL's own switch: `?confviews=false`. */

What I found

Richard is right. isTemplatingEnabled reads templating-config in default. A user who can't read it gets undefined, which counts as on, so turning it off never reaches them.

What I'll do

See Decision 1. I recommend Richard's alternative: the global flag plus a temporary runtime off.

Draft reply

Going with your alternative. ?confviews=false already gives the temporary runtime off, so I removed the stored switch, its shortcut and the Settings page. Checking the feature flag stays on your TBD list, since it's off by default and hidden from the Feature Flags list.

30

Make clear tables don't update

MediumYour call
Bringing across https://github.com/rancher/dashboard/pull/19293#discussion_r4123918310 Tracked elsewhere for discussion. We'll need to make it clear these tables do not update (given every other visually same table does update). Ideally we allow the user to manually refresh
The code it points at
109    };
110  }
111 
112  const res = await store.dispatch('management/findPage', {
113    type: resource,
114    opt:  {
115      url:        `/k8s/clusters/${ encodeURIComponent(cluster) }/v1/${ resource }`,

What I found

A table reading from a cluster is read once (transient: true, watch: false). On dashboard PR 19293 I answered that these are a snapshot read when the view opens. Richard is right that it needs to show, because they look like every other table.

What I'll do

See Decision 2, which has a mock-up.

Draft reply

Here's what I have in mind: a small "Not live: read at 14:02" with a Refresh button on widgets that read once (mock-up attached). Does that work for you and UX?

31

UX: discuss these pages

None yetWaits on UX
Tracking here UX: Discussion on these pages required
The code it points at
12 
13// Its two pages. The one on top is where the product opens, so Settings - the kill switch - leads:
14// always reachable, including when the feature is off.
15export const pages: ProductChildCustomPage[] = [
16  {
17    name:      PAGE_SETTINGS,
18    labelKey:  'configurableViews.settings.label',

What I found

The product has two pages. Settings holds only the switch from thread 29. Configurable Views lists everything saved per page, with Edit YAML. If Decision 1 goes as I recommend, Settings leaves the base, which answers half of Richard's "is there a need for the global product page". The other page is useful to us for debugging. Whether anyone else needs it is the UX question.

Settings: only the switch.
Settings: only the switch.
Configurable Views: everything saved for each page.
Configurable Views: everything saved for each page.

What I'll do

No code until UX answers.

32

Should bar visibility be a preference?

None yetWaits on UX
minor, one for UX. should this be a user preference?
The code it points at
1import { ref } from 'vue';
2 
3/** Per browser: whether the bar shows is a matter of taste, not something to sync between people. */
4const STORAGE_KEY = 'configurable-views-bar-visible';
5 
6function read(): boolean {

What I found

It's in localStorage today, per browser, on purpose. A preference would follow the person between machines. It's a small change either way.

What I'll do

No change until UX answers.

Everything in the review body

Issues are disabled on the repo, so the Later and TBD items stay in Richard's review unless you want them filed somewhere else.

TagRichard's itemWhat happens
Discussion
MVPUX/Dev: how to handle automatic resource updates. At the moment the data on all dashboards does not auto-update. For the first pass we need to make this clear to usersPropose the mock-up, then build it. Your call in Decision 2
MVPUX/Dev: is there a need for the global product pageWaits on UX (thread 31). Decision 1 answers half of it
LaterUX: Improve widget catlog catagorisation. Add tags for type (group/layout widget, summary data widget, location specific widget)Not touching
Improvements
MVPHide views bar (with tabs) shortcut is currently ctrl + shift + v which clashes with unformatted pasteFix: Shift+V (M3)
LaterIntegrate SSP (important for prod)Not touching. Explained in thread 26
LaterConfiguring home page table components shouldn't show the 'this pages cluster' option for dataNot touching
TBDPersisting Global Views to a new CRD (similar to super charged tables)Not in this PR. See Decision 4, because it blocks the QA list
TBDPersonal Views to persist to new process (similar to user preferences) or user preferencesNot in this PR. See Decision 4
TBDPersona IntegrationNot touching
LaterCluster specific Views (via label selector)Not touching
LaterTable Widget does not use the user preference table row countNot touching
MVPMake extension prime onlyFix: the annotation (thread 5)
TBDDisable feature on feature flag disabledNot now. The flag is off by default and hidden from the Feature Flags list, so gating on it hides the feature everywhere we test. See Decision 1
LaterAdd a share / load feature to enable individual users to swap viewsNot touching
Bugs
MVPfresh instance, no views, there's a noticable (+3 second) delay when navigating to home/explorer dashboard pageFix. Reproduced (M1)
MVPDelta between new Cluster Explorer Dashboard components and stock Cluster Explorer Dashboard componentsFix. Five differences (M2)
LaterWhen editing a view and the user collapses and expands the edit view section there's a console warning [Vue warn]: injection "Symbol(formSummary)" not found. could be an existing (not introduced by PR)?Not touching. The symbol is in the shell's TableOfContents/composables.ts, and our package doesn't mention it, so it may well be existing. I haven't traced it
Areas For QA
QAHome Page: Create a table widget pointing to a downstream cluster. delete the cluster, the page should handle this in a reasonable wayLooks reasonable, tested with a made-up cluster id. See below
QAExisting Home Page (stock) functionality has not regressedI'll check after the rebuild
QAExisting Cluster Explorer Dashboard (stock) functionality (including monitoring) has not regressedI'll check after the rebuild. This needs a cluster that really has monitoring: local has none and created-dashboard-2 has only the CRD chart
QAuser scenarios - each can manage views on both dashboards: non-admin, stadard-user, cluster member; non-admin, user-base, project memberFails today, for both. See Decision 4

The deleted-cluster case from the QA list

I pointed a Home table at a cluster id that doesn't exist, in my browser only. The widget says "Loading…" for 14 seconds (Rancher answers 500), then shows the error. The other widget and the rest of the page keep working. That seems reasonable to me. Two small things I could polish if you want: it shows the id because the name is gone, and a "0" sits beside the title.

A table whose cluster no longer exists, next to one that's fine.
A table whose cluster no longer exists, next to one that's fine.

How it ships and how I'll check it

What I checked for this page

I checked every thread against the code at 0f34877. These I also ran:

ClaimHowResult
The delayPlaywright, with the ConfigMap read stubbed, 3 runs per case, against the master previewReproduced
Non-admin accesskubectl --as with 6 users on mf, writes with --dry-run=serverConfirmed Forbidden, except a local cluster owner
What a user with no access seesAdmin session with the configmap schema removed from the responseSimulated: stock tab only, Save fails
Input centringPixel gaps from 3x screenshots in 3 engines, then an injected styleUnder 1px, and the padding fix works
Wrong-cluster linksA stubbed view on created-dashboard-2's dashboardConfirmed by reading the links' href
Differences from stockSame cluster and window size, master preview against the base previewFive found. To redo on the same shell
Deleted clusterA made-up cluster id in a stubbed viewHandled

Every browser run blocked ConfigMap writes, and the stubs lived in my browser, so nothing changed on the shared server.

Not checked