Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
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
48 changes: 43 additions & 5 deletions web-admin/src/features/dashboards/listing/DashboardsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
import DashboardsTagSidebar from "@rilldata/web-admin/features/dashboards/listing/DashboardsTagSidebar.svelte";
import { filterResources } from "@rilldata/web-common/features/resources/resource-filter-utils.ts";
import { Throttler } from "@rilldata/web-common/lib/throttler.ts";
import {
getDashboardFavouritesStore,
RecentlyUsedDashboards,
} from "@rilldata/web-admin/features/dashboards/listing/dashboard-favourites.ts";
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
import { escapeHtml } from "@rilldata/web-common/lib/i18n";

let {
isEmbedded = false,
isPreview = false,
previewLimit = 5,
previewLimit = undefined,
}: {
isEmbedded?: boolean;
isPreview?: boolean;
Expand All @@ -32,7 +36,7 @@

const selectedTagsState = UrlParamsState.createStringArrayParam("tags");

const searchTextState = UrlParamsState.createStringParam("search");
const searchTextState = UrlParamsState.createStringParam("q");
const throttler = new Throttler(500, 500);
const throttledSearchSetter = (newValue: string) => {
throttler.throttle(() => searchTextState.setter(newValue));
Expand Down Expand Up @@ -67,14 +71,33 @@
),
);

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(
dashboardFavourites.value.filter((f) =>
filteredDashboards.find((r) => r.meta?.name?.name?.toLowerCase() === f),
),
);

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

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

const columns = [
{
id: "composite",
Expand Down Expand Up @@ -104,6 +127,8 @@
organization,
project,
tags,
dashboardFavourites,
recentlyUsedDashboards,
});
},
},
Expand Down Expand Up @@ -136,16 +161,26 @@
return isMetricsExplorer ? row.explore.spec.description : "";
},
},
{
id: "lastUsed",
accessorFn: (row: V1Resource) => {
return (row as any).lastUsed;
},
},
];

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

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

{#if isLoading || isBuilding}
Expand All @@ -164,6 +199,7 @@
bind:value={searchTextState.getter, throttledSearchSetter}
rounded="lg"
retainValueOnMount
large
/>
</div>
{/if}
Expand Down Expand Up @@ -193,6 +229,8 @@
{columnVisibility}
{initialSorting}
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 "@rilldata/web-admin/features/dashboards/listing/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,29 @@
$: 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 onDashboardFavouriteToggle(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
dashboardFavourites?.toggle(name?.toLowerCase());
}
</script>

<a class="flex flex-col gap-y-1 group px-4 py-2.5 w-full h-full" {href}>
<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 +71,14 @@
{#each tags as tag (tag)}
<Tag color="gray">{tag}</Tag>
{/each}
<div class="grow"></div>
{#if dashboardFavourites && (hovered || isFavourite)}
<Star
size="16px"
class={isFavourite ? "fill-accent-primary-action" : "none"}
onclick={onDashboardFavouriteToggle}
/>
{/if}
</div>
<div
class="flex gap-x-1 text-fg-tertiary text-xs font-normal min-h-[16px] overflow-hidden"
Expand All @@ -62,6 +97,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,9 @@
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 } from "@rilldata/web-admin/features/dashboards/listing/dashboard-favourites.ts";
import { page } from "$app/state";

let {
align = "start",
Expand All @@ -28,6 +29,21 @@
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(
[...availableTags].sort((a, b) => {
let aIndex = tagsFavourites.value.indexOf(a.name);
if (aIndex === -1) aIndex = tagsFavourites.value.length;
let bIndex = tagsFavourites.value.indexOf(b.name);
if (bIndex === -1) bIndex = tagsFavourites.value.length;
return aIndex - bIndex;
}),
);
</script>

{#if availableTags.length > 0}
Expand All @@ -45,7 +61,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
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
<script lang="ts">
import * as Tooltip from "@rilldata/web-common/components/tooltip-v2";
import { detectOverflow } from "@rilldata/web-common/lib/actions/detect-overflow";
import { Star } from "lucide-svelte";

let {
name,
count,
selected,
isFavourite,
onToggle,
onFavouriteToggle,
}: {
name: string;
count: number;
selected: boolean;
isFavourite: boolean;
onToggle: () => void;
onFavouriteToggle: () => void;
} = $props();

// Only show the tooltip when the tag name is actually clipped, matching the
// pivot tag rows.
let isTruncated = $state(false);

let hovered = $state(false);

function handleFavouriteToggle(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
onFavouriteToggle();
}
</script>

<button class="tag-row" class:selected onclick={onToggle}>
<button
class="tag-row"
class:selected
onclick={onToggle}
onmouseenter={() => (hovered = true)}
onmouseleave={() => (hovered = false)}
>
<Tooltip.Root delayDuration={200} disabled={!isTruncated}>
<Tooltip.Trigger>
{#snippet child({ props })}
Expand All @@ -40,6 +59,16 @@
</Tooltip.Content>
</Tooltip.Root>

{#if hovered || isFavourite}
<Star
size="14px"
class="text-accent-primary-action {isFavourite
? 'fill-accent-primary-action'
: ''}"
onclick={handleFavouriteToggle}
/>
{/if}

<span class="count-tile" title={`${count} dashboards`}>{count}</span>
</button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import DashboardsTagRow from "./DashboardsTagRow.svelte";
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 { getDashboardTagFavouritesStore } from "@rilldata/web-admin/features/dashboards/listing/dashboard-favourites.ts";
import { page } from "$app/state";
import { flip } from "svelte/animate";

let {
resources,
Expand All @@ -25,21 +28,40 @@
)
: tags,
);

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

let sortedTags = $derived(
[...filteredTags].sort((a, b) => {
let aIndex = tagsFavourites.value.indexOf(a.name);
if (aIndex === -1) aIndex = tagsFavourites.value.length;
let bIndex = tagsFavourites.value.indexOf(b.name);
if (bIndex === -1) bIndex = tagsFavourites.value.length;
return aIndex - bIndex;
}),
);
</script>

<div class="tags-scroll">
<h3 class="column-header">Tags</h3>

{#if filteredTags.length === 0}
{#if sortedTags.length === 0}
<p class="text-fg-secondary my-1 px-2 text-xs">No matching tags</p>
{:else}
{#each filteredTags as tag (tag.name)}
<DashboardsTagRow
name={tag.name}
count={tag.totalCount}
selected={selectedTagsState.value.includes(tag.name)}
onToggle={() => selectedTagsState.toggle(tag.name)}
/>
{#each sortedTags as tag (tag.name)}
<div animate:flip={{ duration: 200 }}>
<DashboardsTagRow
name={tag.name}
count={tag.totalCount}
selected={selectedTagsState.value.includes(tag.name)}
isFavourite={tagsFavourites.value.includes(tag.name)}
onToggle={() => selectedTagsState.toggle(tag.name)}
onFavouriteToggle={() => tagsFavourites.toggle(tag.name)}
/>
</div>
{/each}
{/if}
</div>
Expand Down
Loading
Loading