Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
88ffa19
feat: add tags to explore and canvas dashboards for project-level org…
royendo Apr 22, 2026
e1baab5
feat: persist dashboards tag filter in URL
royendo Apr 22, 2026
cc69880
feat: add tagAsFolders feature flag for folder-style dashboard organi…
royendo Apr 22, 2026
1fd640f
fix: hide tag filter in folder mode; unify folders under one parent b…
royendo Apr 22, 2026
3f17d37
fix: hide tag pills on dashboard rows in folder mode
royendo Apr 22, 2026
9381af7
fix: restore dashboard breadcrumb and hide tag pills when tagAsFolder…
royendo Apr 22, 2026
1488ea8
fix: preserve non-explore URL params (e.g. ?tags=) across explore sta…
royendo Apr 22, 2026
98626eb
feat: always show tag folder breadcrumb on dashboard pages
royendo Apr 22, 2026
001fa6d
fix: scope dashboard breadcrumb dropdown to the active tag folder
royendo Apr 22, 2026
bf47792
Update DashboardStateSync.ts
royendo Apr 23, 2026
ab4b206
tags to top level;
royendo Apr 23, 2026
8a51dde
Merge branch 'main' into worktree-feat-project-dashboard-organization
royendo Apr 23, 2026
30acdfa
feat: tag filter for Resources and Tables pages
royendo Apr 23, 2026
3955a43
Merge remote-tracking branch 'origin/main' into worktree-feat-project…
royendo Apr 24, 2026
88bc52d
Update resource-filter-utils.spec.ts
royendo Apr 24, 2026
e4cc524
fix: declare `availableTags` before `filterGroups` reads it
royendo Apr 24, 2026
1a2b3f1
Merge remote-tracking branch 'origin/main' into worktree-feat-project…
royendo Apr 27, 2026
4546946
fix
royendo Apr 27, 2026
0b99483
fix: drop duplicate `onFilterChange` declaration in `ProjectTables`
royendo Apr 27, 2026
2c6c70f
fix: render "Not Tagged" label and add `tags` to `V1ResourceMeta`
royendo Apr 27, 2026
fc452bd
nit: comment update
begelundmuller May 15, 2026
13e6679
Merge branch 'main' into worktree-feat-project-dashboard-organization
AdityaHegde May 15, 2026
6df43e7
self review
AdityaHegde May 28, 2026
c4ea4e1
Merge branch 'main' into worktree-feat-project-dashboard-organization
AdityaHegde May 28, 2026
de8e265
Add tests for tag based grouping logic
AdityaHegde May 28, 2026
fec19ee
Merge branch 'main' into worktree-feat-project-dashboard-organization
AdityaHegde Jun 29, 2026
f00e922
Merge branch 'main' into worktree-feat-project-dashboard-organization
AdityaHegde Jun 30, 2026
2ea6b8a
Move from feature flags to presence of tags
AdityaHegde Jun 30, 2026
5932a3f
Fix untagged dashboards
AdityaHegde Jun 30, 2026
e538a64
feat: dashboard favourites
AdityaHegde Jun 30, 2026
c8fe799
Add tag dropdown in home page
AdityaHegde Jul 2, 2026
92749de
Improve tag to url persistance
AdityaHegde Jul 2, 2026
ccea7e5
Add sidebar for dashboards table
AdityaHegde Jul 3, 2026
888aa46
Revert tag based folders in breadcrumbs
AdityaHegde Jul 3, 2026
78591dc
Add tag selection and search to dashboard dropdown
AdityaHegde Jul 6, 2026
4243d43
Merge branch 'main' into worktree-feat-project-dashboard-organization
AdityaHegde Jul 6, 2026
f6d30fb
Generate docs
AdityaHegde Jul 6, 2026
4ea001a
Merge branch 'worktree-feat-project-dashboard-organization' into feat…
AdityaHegde Jul 6, 2026
9c29d05
Cleanup store management
AdityaHegde Jul 6, 2026
89e360f
Merge branch 'worktree-feat-project-dashboard-organization' into feat…
AdityaHegde Jul 6, 2026
d232c4f
Use pinning for favourites
AdityaHegde Jul 6, 2026
658d59e
Merge branch 'main' into feat/dashboard-faviourites
AdityaHegde Jul 8, 2026
10fe792
Add tags favourites
AdityaHegde Jul 8, 2026
2cafbbe
Merge branch 'main' into feat/dashboard-faviourites
AdityaHegde Jul 9, 2026
d32b1fb
Add last used framework in local storage
AdityaHegde Jul 9, 2026
b48ce06
Merge branch 'main' into feat/dashboard-faviourites
AdityaHegde Jul 13, 2026
f4df65e
Add toggling sorting dropdown
AdityaHegde Jul 13, 2026
1cd0b3c
Merge branch 'main' into feat/dashboard-faviourites
AdityaHegde Jul 16, 2026
12a13df
Improve sort with order of sort
AdityaHegde Jul 16, 2026
dc6f085
Fix faviourite toggle
AdityaHegde Jul 16, 2026
03f480b
Merge branch 'main' into feat/dashboard-faviourites
nishantmonu51 Jul 16, 2026
b3e4239
Move to a simpler sort selector
AdityaHegde Jul 17, 2026
9374bb3
self review
AdityaHegde Jul 17, 2026
21f30d5
Fix sort defaults
AdityaHegde Jul 21, 2026
f4368ad
Add caret icon
AdityaHegde Jul 22, 2026
57ead88
i18n
AdityaHegde Jul 22, 2026
632edb5
PR comments
AdityaHegde Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .claude/skills/i18n-migrate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
description: Find hardcoded user-facing strings in the branch's changes and migrate them to i18n (paraglide messages), following the repo's i18n conventions
allowed-tools: Bash(git:*), Bash(npm:*), Glob, Grep, Read, Edit, AskUserQuestion
argument-hint: "[optional: files/dirs to narrow focus]"
---

Audit the changes on the current branch for hardcoded user-facing strings that should be moved to i18n (Paraglide/inlang messages), following the conventions in `web-common/src/lib/i18n/README.md`.

Optional focus: $ARGUMENTS

## Instructions

### 1. Determine the diff to audit

- If `$ARGUMENTS` lists files or directories, scope the audit to those paths only.
- Otherwise, audit the PR diff: `git diff --merge-base main` to get changed lines across the branch.
- Only consider **added or modified** lines. Do not flag pre-existing strings unless the file was explicitly passed in `$ARGUMENTS`.
- Only frontend files are in scope: `web-common/`, `web-admin/`, `web-local/`, with extensions `.svelte` and `.ts`/`.svelte.ts`.

### 2. Find hardcoded user-facing strings

Mirror the heuristics in `scripts/i18n-guard.js`:

- **Svelte visible text**: content between `>` and `<` that contains real words.
- **Human-facing attributes**: `placeholder`, `title`, `aria-label`, `alt`, `label`.
- **TypeScript**: string literals used as user-facing copy — e.g. `label:`, `header:`, `body:`, toast/notification text, thrown error messages shown to users, option arrays.

**Skip** (not user-facing): identifiers, URLs, paths, `CONSTANT_CASE`, class/style names, object keys, `console.*`/log messages, code comments, test files (`*.spec.ts`, `*.test.ts`, e2e), and lines with an `i18n-ignore` comment on or above them.

### 3. Propose keys, reusing what exists

For each finding, before proposing a new key, grep `web-common/src/lib/i18n/messages/en.json` for the exact copy — **reuse an existing key** if one matches (especially `common_*`).

When a new key is needed, follow the README conventions:

- Naming: `feature_component_purpose`, lower snake_case, grouped by prefix.
- `common_*` for copy reused across features; otherwise prefix with the feature directory name.
- **Generic shared components** (in `web-common/src/components/`) should use `common_*` keys, not feature-specific ones.
- **Interpolation**: named placeholders, never string concatenation — `"Sort by {label}"`, not `"Sort by " + label`.
- **Pluralization**: use Paraglide variants, not `count === 1 ? ... : ...`.

### 4. Watch for these patterns

- **Module-level `const` in `.ts`** (e.g. an options array with `label`): calling `m.key()` at module load freezes the locale. Use a getter — `get label() { return m.key(); }` — so it resolves lazily and stays locale-reactive.
- **Composed labels**: parameterize the whole string as one interpolated message rather than concatenating a translated prefix with a value.

### 5. Report

Present a table ordered by significance: `file:line`, the current string, the suggested key (or existing key to reuse), and whether interpolation/variants are needed. Note any strings that are borderline or intentionally left as-is.

### 6. Offer to migrate

Ask whether to apply the migration. If yes, for each string:

1. Add the key to **both** `messages/en.json` and `messages/es.json`, placed alphabetically within its prefix group. Provide a real Spanish translation for `es.json`.
2. Replace the literal in code with `m.key()` / `m.key({ var })`.
3. Run `npm run build:i18n` to compile and confirm the message functions generate cleanly.
4. If the change fully migrates a new area, append its directory to `MIGRATED_GLOBS` in `scripts/i18n-guard.js`.
78 changes: 61 additions & 17 deletions web-admin/src/features/dashboards/listing/DashboardsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,28 @@
import { renderComponent } from "tanstack-table-8-svelte-5";
import DashboardsTableCompositeCell from "./DashboardsTableCompositeCell.svelte";
import { useDashboards, useIsInitialBuild } from "./selectors";
import { Search } from "@rilldata/web-common/components/search";
import { UrlParamsState } from "web-common/src/lib/store-utils/url-params-state.svelte.ts";
import { getAllTagsForResources } from "@rilldata/web-common/features/resources/resource-tag-utils.ts";
import ResizableSidebar from "@rilldata/web-common/layout/ResizableSidebar.svelte";
import DashboardsTagSidebar from "@rilldata/web-admin/features/dashboards/listing/DashboardsTagSidebar.svelte";
import { filterResources } from "@rilldata/web-common/features/resources/resource-filter-utils.ts";
import {
DashboardTableSortOptions,
getDashboardFavouritesStore,
RecentlyUsedDashboards,
} from "./dashboard-favourites.ts";
import { DebouncedRuneStore } from "@rilldata/web-common/lib/store-utils/types.svelte.ts";
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
import { escapeHtml } from "@rilldata/web-common/lib/i18n";
import { TableToolbar } from "@rilldata/web-common/components/table-toolbar";
import { dedupe } from "@rilldata/web-common/lib/arrayUtils.ts";

type DashboardRow = V1Resource & { lastUsed: number };

let {
isEmbedded = false,
isPreview = false,
previewLimit = 5,
previewLimit = undefined,
}: {
isEmbedded?: boolean;
isPreview?: boolean;
Expand All @@ -37,6 +45,16 @@
500,
);

const sortStore = UrlParamsState.createStringParam(
"sort",
DashboardTableSortOptions[0].value,
);
let sortingOption = $derived(
DashboardTableSortOptions.find(
(option) => option.value === sortStore.value,
) ?? DashboardTableSortOptions[0],
);

const runtimeClient = useRuntimeClient();
let { organization, project } = $derived(page.params);

Expand Down Expand Up @@ -66,14 +84,38 @@
),
);

let displayData = $derived(
isPreview ? filteredDashboards.slice(0, previewLimit) : filteredDashboards,
let dashboardFavourites = $derived(
getDashboardFavouritesStore(organization, project),
);
let recentlyUsedDashboards = $derived(
new RecentlyUsedDashboards(organization, project),
);

let validDashboardFavourites = $derived(
dedupe(
dashboardFavourites.value.filter((f) =>
filteredDashboards.find((r) => r.meta?.name?.name?.toLowerCase() === f),
),
(e) => e,
),
);

let hasMoreDashboards = $derived(
isPreview && filteredDashboards.length > previewLimit,
);

let displayData = $derived(
filteredDashboards.map(
(r): DashboardRow => ({
...r,
lastUsed:
recentlyUsedDashboards.recentlyUsed.value[
r.meta?.name?.name?.toLowerCase() ?? ""
] ?? 0,
}),
),
);

const columns = [
{
id: "composite",
Expand Down Expand Up @@ -103,6 +145,8 @@
organization,
project,
tags,
dashboardFavourites,
recentlyUsedDashboards,
});
},
},
Expand Down Expand Up @@ -135,16 +179,19 @@
return isMetricsExplorer ? row.explore.spec.description : "";
},
},
{
id: "lastUsed",
accessorFn: (row: DashboardRow) => row.lastUsed,
},
];

const columnVisibility = {
title: false,
name: false,
lastRefreshed: false,
description: false,
lastUsed: false,
};

const initialSorting = [{ id: "name", desc: false }];
</script>

{#if isLoading || isBuilding}
Expand All @@ -156,16 +203,11 @@
{:else if isSuccess}
<div class="flex flex-col w-full gap-y-3">
{#if !isPreview}
<div class="flex flex-row items-center gap-x-2">
<Search
placeholder={m.common_search()}
autofocus={false}
bind:value={searchTextStore.getter, searchTextStore.setter}
rounded="lg"
retainValueOnMount
large
/>
</div>
<TableToolbar
{searchTextStore}
{sortStore}
sortOptions={DashboardTableSortOptions}
/>
{/if}

<div class="flex flex-row flex-1 w-full gap-x-2 overflow-hidden">
Expand All @@ -191,8 +233,10 @@
data={displayData}
{columns}
{columnVisibility}
{initialSorting}
sorting={[sortingOption.sort]}
Comment thread
AdityaHegde marked this conversation as resolved.
toolbar={false}
pinnedRows={validDashboardFavourites}
maxRows={previewLimit}
>
<ResourceListEmptyState
slot="empty"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import ResourceTypeBadge from "@rilldata/web-common/features/entity-management/ResourceTypeBadge.svelte";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import { timeAgo } from "@rilldata/web-common/lib/time/relative-time";
import { Star } from "lucide-svelte";
import { ArrayRuneStore } from "web-common/src/lib/store-utils/types.svelte.ts";
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
import type { RecentlyUsedDashboards } from "./dashboard-favourites.ts";

export let name: string;
export let title: string;
Expand All @@ -17,6 +20,10 @@
export let organization: string;
export let project: string;
export let tags: string[] = [];
export let dashboardFavourites: ArrayRuneStore<string> | undefined =
undefined;
export let recentlyUsedDashboards: RecentlyUsedDashboards | undefined =
undefined;

$: lastRefreshedDate = lastRefreshed ? new Date(lastRefreshed) : null;

Expand All @@ -28,9 +35,41 @@
$: resourceKind = isMetricsExplorer
? ResourceKind.Explore
: ResourceKind.Canvas;

$: favourites = dashboardFavourites?.value ?? [];
$: isFavourite = favourites.includes(name?.toLowerCase());

$: lastUsed =
recentlyUsedDashboards?.recentlyUsed?.value?.[name.toLowerCase()];
$: lastUsedDate = lastUsed ? new Date(lastUsed) : null;

let hovered = false;

function toggleFavourite() {
dashboardFavourites?.toggle(name?.toLowerCase());
}

function onDashboardFavouriteToggle(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
toggleFavourite();
}

function onDashboardFavouriteKeydown(e: KeyboardEvent) {
if (e.key !== "Enter" && e.key !== " ") return;
e.stopPropagation();
e.preventDefault();
toggleFavourite();
}
</script>

<a class="flex flex-col gap-y-1 group px-4 py-2.5 w-full h-full" {href}>
<!-- TODO: Support non-hover actions like using keyboard for showing favourite icon -->
<a
class="flex flex-col gap-y-1 group px-4 py-2.5 w-full h-full"
{href}
onmouseenter={() => (hovered = true)}
onmouseleave={() => (hovered = false)}
>
<div class="flex gap-x-2 items-center min-h-[20px]">
<ResourceTypeBadge kind={resourceKind} />
<span
Expand All @@ -44,6 +83,25 @@
{#each tags as tag (tag)}
<Tag color="gray">{tag}</Tag>
{/each}
<div class="grow"></div>
{#if dashboardFavourites && (hovered || isFavourite)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep dashboard favourites keyboard-reachable

For dashboards that are not already favourited, the star is only rendered while hovered is true. Keyboard and touch users never create that hover state before the control exists, so they can navigate/open dashboards but cannot add a favourite from this list. Keep the control in the DOM, or reveal it on focus/touch, while hiding it visually as needed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a problem across the app (eg: bookmarks), will do a follow up to fix.

<span
role="button"
tabindex="0"
aria-label={isFavourite
? m.dashboard_remove_favourite()
: m.dashboard_add_favourite()}
aria-pressed={isFavourite}
class="shrink-0 flex cursor-pointer"
onclick={onDashboardFavouriteToggle}
onkeydown={onDashboardFavouriteKeydown}
>
<Star
size="16px"
class={isFavourite ? "fill-accent-primary-action" : "none"}
/>
</span>
{/if}
</div>
<div
class="flex gap-x-1 text-fg-tertiary text-xs font-normal min-h-[16px] overflow-hidden"
Expand All @@ -62,6 +120,19 @@
</TooltipContent>
</Tooltip>
{/if}
{#if lastUsedDate}
<span class="shrink-0">•</span>
<Tooltip distance={8}>
<span class="shrink-0"
>{m.dashboard_last_used_ago({
time: timeAgo(lastUsedDate),
})}</span
>
<TooltipContent slot="tooltip-content">
{lastUsedDate.toLocaleString()}
</TooltipContent>
</Tooltip>
{/if}
{#if description}
<span class="shrink-0">•</span>
<span class="truncate">{description}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
getAllTagsForResources,
getTagFilterLabel,
} from "@rilldata/web-common/features/resources/resource-tag-utils.ts";

import type { ArrayRuneStore } from "web-common/src/lib/store-utils/types.svelte.ts";
import {
getDashboardTagFavouritesStore,
sortByFavourites,
} from "./dashboard-favourites.ts";
import { page } from "$app/state";

let {
align = "start",
Expand All @@ -28,6 +32,15 @@
let availableTags = $derived(getAllTagsForResources($dashboards?.data ?? []));

let tagsLabel = $derived(getTagFilterLabel(selectedTagsStore.value));

let { organization, project } = $derived(page.params);
let tagsFavourites = $derived(
getDashboardTagFavouritesStore(organization, project),
);

let sortedTags = $derived(
sortByFavourites(availableTags, tagsFavourites.value, (t) => t.name),
);
</script>

{#if availableTags.length > 0}
Expand All @@ -45,7 +58,7 @@
{/if}
</DropdownMenu.Trigger>
<DropdownMenu.Content {align} class="w-48 max-h-72 overflow-y-auto">
{#each availableTags as tag (tag.name)}
{#each sortedTags as tag (tag.name)}
<DropdownMenu.CheckboxItem
checked={selectedTagsStore.value.includes(tag.name)}
onCheckedChange={() => selectedTagsStore.toggle(tag.name)}
Expand Down
Loading
Loading