Skip to content

Commit

Permalink
Cloud UI: Surface Custom Dashboards in navigation (#4531)
Browse files Browse the repository at this point in the history
* WIP commit

* Fix lint

* Use TanStack Query client to fetch chart data

* TODO: render a "read-only" version of the dashboard

* Utils to differentiate between Metrics Explorer and Custom Dashboard pages

* Show both Metrics Explorers and Custom Dashboards in the breadcrumb

* Remove console log

* Add wrongly-detected TS error to whitelist

* Small clean-up

* Fix merge

* Remove unused component
  • Loading branch information
ericpgreen2 authored Apr 16, 2024
1 parent 6b2e0b4 commit ce70116
Show file tree
Hide file tree
Showing 19 changed files with 200 additions and 61 deletions.
1 change: 1 addition & 0 deletions scripts/tsc-with-whitelist.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ web-admin/src/features/scheduled-reports/selectors.ts: error TS2345
web-admin/src/features/view-as-user/clearViewedAsUser.ts: error TS18047
web-admin/src/features/view-as-user/clearViewedAsUser.ts: error TS2322
web-admin/src/features/view-as-user/setViewedAsUser.ts: error TS2322
web-admin/src/routes/[organization]/[project]/-/dashboards/+page.ts: error TS2307
web-common/src/components/button-group/ButtonGroup.spec.ts: error TS2345
web-common/src/components/data-graphic/actions/mouse-position-to-domain-action-factory.ts: error TS2322
web-common/src/components/data-graphic/actions/outline.ts: error TS18047
Expand Down
30 changes: 22 additions & 8 deletions web-admin/src/features/dashboards/listing/DashboardsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { type Row, flexRender } from "@tanstack/svelte-table";
import { flexRender, type Row } from "@tanstack/svelte-table";
import { createEventDispatcher } from "svelte";
import Table from "../../../components/table/Table.svelte";
import DashboardsError from "./DashboardsError.svelte";
import DashboardsTableCompositeCell from "./DashboardsTableCompositeCell.svelte";
import DashboardsTableEmpty from "./DashboardsTableEmpty.svelte";
import DashboardsTableHeader from "./DashboardsTableHeader.svelte";
import NoDashboardsCTA from "./NoDashboardsCTA.svelte";
import { type DashboardResource, useDashboardsV2 } from "./selectors";
import { useDashboardsV2, type DashboardResource } from "./selectors";
export let isEmbedded = false;
Expand All @@ -30,13 +30,27 @@
{
id: "composite",
cell: ({ row }) => {
const dashboard = row.original as DashboardResource;
const dashboardResource = row.original as DashboardResource;
const resource = dashboardResource.resource;
const refreshedOn = dashboardResource.refreshedOn;
const name = resource.meta.name.name;
// If not a Metrics Explorer, it's a Custom Dashboard.
const isMetricsExplorer = !!resource?.metricsView;
const title = isMetricsExplorer
? resource.metricsView.spec.title
: resource.dashboard.spec.title;
const description = isMetricsExplorer
? resource.metricsView.spec.description
: "";
return flexRender(DashboardsTableCompositeCell, {
name: dashboard.resource.meta.name.name,
title: dashboard.resource.metricsView.spec.title,
lastRefreshed: dashboard.refreshedOn,
description: dashboard.resource.metricsView.spec.description,
error: dashboard.resource.meta.reconcileError,
name,
title,
lastRefreshed: refreshedOn,
description,
error: resource.meta.reconcileError,
isMetricsExplorer,
isEmbedded,
});
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from "$app/stores";
import DashboardIcon from "@rilldata/web-common/components/icons/DashboardIcon.svelte";
import CustomDashboardIcon from "@rilldata/web-common/components/icons/CustomDashboardIcon.svelte";
import MetricsExplorerIcon from "@rilldata/web-common/components/icons/MetricsExplorerIcon.svelte";
import Tag from "@rilldata/web-common/components/tag/Tag.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
Expand All @@ -11,23 +12,34 @@
export let lastRefreshed: string;
export let description: string;
export let error: string;
export let isMetricsExplorer: boolean;
export let isEmbedded: boolean;
$: organization = $page.params.organization;
$: project = $page.params.project;
$: lastRefreshedDate = new Date(lastRefreshed);
$: isValidLastRefreshedDate = !isNaN(lastRefreshedDate.getTime());
$: href = isEmbedded
? undefined
: isMetricsExplorer
? `/${organization}/${project}/${name}`
: `/${organization}/${project}/-/dashboards/${name}`;
</script>

<svelte:element
this={isEmbedded ? "button" : "a"}
class="flex flex-col gap-y-0.5 group px-4 py-[5px] w-full"
href={isEmbedded ? undefined : `/${organization}/${project}/${name}`}
{href}
role={isEmbedded ? "button" : "link"}
>
<div class="flex gap-x-2 items-center">
<DashboardIcon size={"14px"} className="text-slate-500" />
{#if isMetricsExplorer}
<MetricsExplorerIcon size={"14px"} className="text-slate-500" />
{:else}
<CustomDashboardIcon size={"14px"} className="text-slate-500" />
{/if}
<div
class="text-gray-700 text-sm font-semibold group-hover:text-primary-600"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import DashboardIcon from "@rilldata/web-common/components/icons/DashboardIcon.svelte";
import MetricsExplorerIcon from "@rilldata/web-common/components/icons/MetricsExplorerIcon.svelte";
</script>

<div
class="m-auto mt-20 pb-10 flex-col justify-center items-center gap-y-4 inline-flex"
>
<div class="flex flex-col justify-center items-center">
<div class="relative">
<DashboardIcon className="text-slate-300 w-12 h-12" />
<MetricsExplorerIcon className="text-slate-300 w-12 h-12" />
</div>
</div>
<div
Expand Down
11 changes: 7 additions & 4 deletions web-admin/src/features/dashboards/listing/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,13 @@ export function useDashboardsV2(
return createRuntimeServiceListResources(instanceId, undefined, {
query: {
select: (data) => {
const dashboards = data.resources.filter((res) => res.metricsView);
return dashboards.map((db) => {
const refreshedOn = getDashboardRefreshedOn(db, data.resources);
return { resource: db, refreshedOn };
// Filter for Metrics Explorers and Custom Dashboards
const resources = data.resources.filter(
(res) => res.metricsView || res.dashboard,
);
return resources.map((resource) => {
const refreshedOn = getDashboardRefreshedOn(resource, data.resources);
return { resource, refreshedOn };
});
},
},
Expand Down
2 changes: 1 addition & 1 deletion web-admin/src/features/embeds/TopNavigationBarEmbed.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import BreadcrumbItem from "@rilldata/web-common/components/navigation/breadcrumbs/BreadcrumbItem.svelte";
import { useValidDashboards } from "@rilldata/web-common/features/dashboards/selectors";
import type {
V1MetricsViewSpec,
Expand All @@ -7,7 +8,6 @@
import { createEventDispatcher } from "svelte";
import LastRefreshedDate from "../dashboards/listing/LastRefreshedDate.svelte";
import { isErrorStoreEmpty } from "../errors/error-store";
import BreadcrumbItem from "@rilldata/web-common/components/navigation/breadcrumbs/BreadcrumbItem.svelte";
export let instanceId: string;
export let activeResourceName: string;
Expand Down
10 changes: 5 additions & 5 deletions web-admin/src/features/errors/error-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { isAdminServerQuery } from "@rilldata/web-admin/client/utils";
import {
isDashboardPage,
isMetricsExplorerPage,
isProjectPage,
} from "@rilldata/web-admin/features/navigation/nav-utils";
import { errorEventHandler } from "@rilldata/web-common/metrics/initMetrics";
Expand Down Expand Up @@ -68,10 +68,10 @@ export function createGlobalErrorCallback(queryClient: QueryClient) {
}
}

// Special handling for some errors on the Dashboard page
const onDashboardPage = isDashboardPage(get(page));
if (onDashboardPage) {
// Let the Dashboard page handle errors for runtime queries.
// Special handling for some errors on the Metrics Explorer page
const onMetricsExplorerPage = isMetricsExplorerPage(get(page));
if (onMetricsExplorerPage) {
// Let the Metrics Explorer page handle errors for runtime queries.
// Individual components (e.g. a specific line chart or leaderboard) should display a localised error message.
// NOTE: let's start with 400 errors, but we may want to include 500-level errors too.
if (
Expand Down
44 changes: 26 additions & 18 deletions web-admin/src/features/navigation/TopNavigationBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
import { page } from "$app/stores";
import Bookmarks from "@rilldata/web-admin/features/bookmarks/Bookmarks.svelte";
import Home from "@rilldata/web-common/components/icons/Home.svelte";
import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte";
import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte";
import OrganizationAvatar from "@rilldata/web-common/components/navigation/breadcrumbs/OrganizationAvatar.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import { useValidVisualizations } from "@rilldata/web-common/features/dashboards/selectors";
import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import {
createAdminServiceGetCurrentUser,
createAdminServiceListOrganizations as listOrgs,
Expand All @@ -13,20 +18,15 @@
import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte";
import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore";
import CreateAlert from "../alerts/CreateAlert.svelte";
import { useAlerts } from "../alerts/selectors";
import AvatarButton from "../authentication/AvatarButton.svelte";
import SignIn from "../authentication/SignIn.svelte";
import LastRefreshedDate from "../dashboards/listing/LastRefreshedDate.svelte";
import ShareDashboardButton from "../dashboards/share/ShareDashboardButton.svelte";
import { isErrorStoreEmpty } from "../errors/error-store";
import ShareProjectButton from "../projects/ShareProjectButton.svelte";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { useReports } from "../scheduled-reports/selectors";
import OrganizationAvatar from "@rilldata/web-common/components/navigation/breadcrumbs/OrganizationAvatar.svelte";
import { isDashboardPage, isProjectPage } from "./nav-utils";
import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte";
import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte";
import { useAlerts } from "../alerts/selectors";
import { useValidDashboards } from "@rilldata/web-common/features/dashboards/selectors";
import { isMetricsExplorerPage, isProjectPage } from "./nav-utils";
const user = createAdminServiceGetCurrentUser();
Expand All @@ -36,7 +36,7 @@
$: ({ organization, project, dashboard, alert, report } = $page.params);
$: onProjectPage = isProjectPage($page);
$: onDashboardPage = isDashboardPage($page);
$: onMetricsExplorerPage = isMetricsExplorerPage($page);
$: organizationQuery = listOrgs(
{ pageSize: 100 },
Expand All @@ -53,14 +53,14 @@
},
});
$: dashboardsQuery = useValidDashboards(instanceId);
$: visualizationsQuery = useValidVisualizations(instanceId);
$: alertsQuery = useAlerts(instanceId);
$: reportsQuery = useReports(instanceId);
$: organizations = $organizationQuery.data?.organizations ?? [];
$: projects = $projectsQuery.data?.projects ?? [];
$: dashboards = $dashboardsQuery.data ?? [];
$: visualizations = $visualizationsQuery.data ?? [];
$: alerts = $alertsQuery.data?.resources ?? [];
$: reports = $reportsQuery.data?.resources ?? [];
Expand All @@ -73,12 +73,20 @@
return map.set(label, { label });
}, new Map<string, PathOption>());
$: dashboardPaths = dashboards.reduce((map, { meta, metricsView }) => {
const id = meta.name.name;
return map.set(id, {
label: metricsView?.state?.validSpec?.title || id,
});
}, new Map<string, PathOption>());
$: visualizationPaths = visualizations.reduce(
(map, { meta, metricsView, dashboard }) => {
const id = meta.name.name;
const isMetricsExplorer = !!metricsView;
return map.set(id, {
label:
(isMetricsExplorer
? metricsView?.state?.validSpec?.title
: dashboard?.spec?.title) || id,
section: isMetricsExplorer ? undefined : "-/dashboards",
});
},
new Map<string, PathOption>(),
);
$: alertPaths = alerts.reduce((map, alert) => {
const id = alert.meta.name.name;
Expand All @@ -99,7 +107,7 @@
$: pathParts = [
organizationPaths,
projectPaths,
dashboardPaths,
visualizationPaths,
report ? reportPaths : alert ? alertPaths : null,
];
Expand Down Expand Up @@ -128,7 +136,7 @@
{#if onProjectPage}
<ShareProjectButton {organization} {project} />
{/if}
{#if onDashboardPage}
{#if onMetricsExplorerPage}
<StateManagersProvider metricsViewName={dashboard}>
<LastRefreshedDate {dashboard} />
{#if $user.isSuccess && $user.data.user}
Expand Down
15 changes: 13 additions & 2 deletions web-admin/src/features/navigation/nav-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ export function isProjectPage(page: Page): boolean {
);
}

export function isDashboardPage(page: Page): boolean {
export function isMetricsExplorerPage(page: Page): boolean {
return (
page.route.id === "/[organization]/[project]/[dashboard]" ||
page.route.id === "/-/embed"
);
}

export function isCustomDashboardPage(page: Page): boolean {
return page.route.id === "/[organization]/[project]/-/dashboards/[dashboard]";
}

/**
* Returns true if the page is any kind of dashboard page (either a Metrics Explorer or a Custom Dashboard).
*/
export function isAnyDashboardPage(page: Page): boolean {
return isMetricsExplorerPage(page) || isCustomDashboardPage(page);
}

export function isReportPage(page: Page): boolean {
return page.route.id === "/[organization]/[project]/-/reports/[report]";
}
Expand All @@ -42,7 +53,7 @@ export function getScreenNameFromPage(page: Page): MetricsEventScreenName {
return MetricsEventScreenName.Organization;
case isProjectPage(page):
return MetricsEventScreenName.Project;
case isDashboardPage(page):
case isMetricsExplorerPage(page):
return MetricsEventScreenName.Dashboard;
case isReportPage(page):
return MetricsEventScreenName.Report;
Expand Down
12 changes: 7 additions & 5 deletions web-admin/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<script lang="ts">
import { beforeNavigate } from "$app/navigation";
import { page } from "$app/stores";
import { isDashboardPage } from "@rilldata/web-admin/features/navigation/nav-utils";
import { isMetricsExplorerPage } from "@rilldata/web-admin/features/navigation/nav-utils";
import { initCloudMetrics } from "@rilldata/web-admin/features/telemetry/initCloudMetrics";
import NotificationCenter from "@rilldata/web-common/components/notifications/NotificationCenter.svelte";
import {
featureFlags,
retainFeaturesFlags,
} from "@rilldata/web-common/features/feature-flags";
import RillTheme from "@rilldata/web-common/layout/RillTheme.svelte";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import { errorEventHandler } from "@rilldata/web-common/metrics/initMetrics";
import { QueryClientProvider } from "@tanstack/svelte-query";
import { onMount } from "svelte";
import ErrorBoundary from "../features/errors/ErrorBoundary.svelte";
import { createGlobalErrorCallback } from "../features/errors/error-utils";
import TopNavigationBar from "../features/navigation/TopNavigationBar.svelte";
import { clearViewedAsUserAfterNavigate } from "../features/view-as-user/clearViewedAsUser";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
// Motivation:
// - https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose#a-bad-api
Expand All @@ -36,11 +36,11 @@
$: isEmbed = $page.url.pathname === "/-/embed";
// The Dashboard component assumes a page height of `h-screen`. This is somehow motivated by
// The Dashboard (Metrics Explorer) component assumes a page height of `h-screen`. This is somehow motivated by
// making the line charts and leaderboards scroll independently.
// However, `h-screen` screws up overflow/scroll on all other pages, so we only apply it to the dashboard.
// (This all feels hacky and should not be considered optimal.)
$: onDashboardPage = isDashboardPage($page);
$: onMetricsExplorerPage = isMetricsExplorerPage($page);
</script>

<svelte:head>
Expand All @@ -49,7 +49,9 @@

<RillTheme>
<QueryClientProvider client={queryClient}>
<main class="flex flex-col min-h-screen {onDashboardPage && 'h-screen'}">
<main
class="flex flex-col min-h-screen {onMetricsExplorerPage && 'h-screen'}"
>
{#if !isEmbed}
<TopNavigationBar />
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types";

export const load: PageLoad = ({ params }) => {
throw redirect(307, `/${params.organization}/${params.project}`);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import { page } from "$app/stores";
$: customDashboardName = $page.params.dashboard;
</script>

<div>
A "read-only" version of the Custom Dashboard named <span
class="font-semibold">{customDashboardName}</span
> goes here.
</div>
Loading

1 comment on commit ce70116

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Please sign in to comment.