Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud UI: Remove calls to ListFiles API #3054

Merged
merged 6 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions web-admin/src/components/errors/error-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export function globalErrorCallback(error: AxiosError): void {
return;
}

// If on a Dashboard page, and "entry not found" (i.e. a dashboard wasn't found),
// ignore the error here, so the page can handle it.
const isDashboardPage =
get(page).route.id === "/[organization]/[project]/[dashboard]";
if (
isDashboardPage &&
error.response.status === 400 &&
(error.response.data as RpcStatus).message === "entry not found"
) {
return;
}

// Create a pretty message for the error page
const errorStoreState = createErrorStoreStateFromAxiosError(error);

Expand Down
14 changes: 7 additions & 7 deletions web-admin/src/components/navigation/Breadcrumbs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
createAdminServiceListOrganizations,
createAdminServiceListProjectsForOrganization,
} from "../../client";
import { useDashboardListItems } from "../projects/dashboards";
import { useDashboards } from "../projects/dashboards";
import BreadcrumbItem from "./BreadcrumbItem.svelte";
import OrganizationAvatar from "./OrganizationAvatar.svelte";

Expand Down Expand Up @@ -43,8 +43,8 @@
);
$: isProjectPage = $page.route.id === "/[organization]/[project]";

$: dashboardListItems = useDashboardListItems(instanceId);
$: currentDashboard = $dashboardListItems?.items?.find(
$: dashboards = useDashboards(instanceId);
$: currentDashboard = $dashboards?.data?.find(
(listing) => listing.name === $page.params.dashboard
);
$: isDashboardPage =
Expand Down Expand Up @@ -92,13 +92,13 @@
{#if currentDashboard}
<span class="text-gray-600">/</span>
<BreadcrumbItem
label={currentDashboard?.title || currentDashboard.name}
label={currentDashboard?.label || currentDashboard.name}
href={`/${orgName}/${projectName}/${currentDashboard.name}`}
menuOptions={$dashboardListItems?.items?.length > 1 &&
$dashboardListItems.items.map((listing) => {
menuOptions={$dashboards?.data?.length > 1 &&
$dashboards.data.map((listing) => {
return {
key: listing.name,
main: listing?.title || listing.name,
main: listing?.label || listing.name,
};
})}
menuKey={currentDashboard.name}
Expand Down
46 changes: 18 additions & 28 deletions web-admin/src/components/projects/DashboardList.svelte
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
<script lang="ts">
import {
DashboardListItem,
getDashboardsForProject,
} from "@rilldata/web-admin/components/projects/dashboards";
import { getDashboardsForProject } from "@rilldata/web-admin/components/projects/dashboards";
import DashboardIcon from "@rilldata/web-common/components/icons/DashboardIcon.svelte";
import { Tag } from "@rilldata/web-common/components/tag";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import type { V1MetricsView } from "@rilldata/web-common/runtime-client";
import {
createAdminServiceGetProject,
V1DeploymentStatus,
V1GetProjectResponse,
} from "../../client";
import ProjectAccessControls from "./ProjectAccessControls.svelte";

export let organization: string;
export let project: string;

let dashboardListItems: DashboardListItem[];
let dashboards: V1MetricsView[];

$: proj = createAdminServiceGetProject(organization, project);
$: if ($proj?.isSuccess && $proj.data?.prodDeployment) {
Expand All @@ -29,24 +23,19 @@
const status = projectData.prodDeployment.status;
if (status === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING) return;

dashboardListItems = await getDashboardsForProject(projectData);
dashboards = await getDashboardsForProject(projectData);
}
</script>

{#if dashboardListItems?.length === 0}
{#if dashboards?.length === 0}
<p class="text-gray-500 text-xs">This project has no dashboards yet.</p>
{:else if dashboardListItems?.length > 0}
{:else if dashboards?.length > 0}
<ol class="flex flex-col gap-y-4 max-w-full 2xl:max-w-[1200px]">
{#each dashboardListItems as dashboardListItem}
{#each dashboards as dashboard}
<li class="w-full h-[52px] border rounded">
<svelte:element
this={dashboardListItem.isValid ? "a" : "div"}
href={dashboardListItem.isValid
? `/${organization}/${project}/${dashboardListItem.name}`
: undefined}
class="w-full h-full overflow-x-auto p-3 flex items-center gap-x-6 {dashboardListItem.isValid
? 'text-gray-700 hover:text-blue-600 hover:bg-slate-50'
: 'text-gray-400'}"
<a
href={`/${organization}/${project}/${dashboard.name}`}
class="w-full h-full overflow-x-auto p-3 flex items-center gap-x-6 text-gray-700 hover:text-blue-600 hover:bg-slate-50"
>
<!-- Icon -->
<div
Expand All @@ -59,13 +48,14 @@
<!-- Name -->
<span
class="text-sm font-medium shrink-0 truncate"
title={dashboardListItem?.title || dashboardListItem.name}
title={dashboard?.label || dashboard.name}
>
{dashboardListItem?.title || dashboardListItem.name}
{dashboard?.label || dashboard.name}
</span>

<!-- We'll show errored dashboards again when we integrate the new Reconcile -->
<!-- Error tag -->
{#if $proj.data.prodDeployment.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_RECONCILING && !dashboardListItem.isValid}
<!-- {#if $proj.data.prodDeployment.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_RECONCILING && !dashboard.isValid}
<Tooltip distance={8} location="right">
<TooltipContent slot="tooltip-content">
<ProjectAccessControls {organization} {project}>
Expand All @@ -85,18 +75,18 @@
</TooltipContent>
<Tag>Error</Tag>
</Tooltip>
{/if}
{/if} -->

<!-- Description -->
{#if dashboardListItem.description}
{#if dashboard.description}
<!-- Note: line-clamp-2 uses `display: -webkit-box;` which overrides the `hidden` class -->
<span
class="text-gray-800 text-xs font-light break-normal hidden sm:line-clamp-2"
>{dashboardListItem.description}</span
>{dashboard.description}</span
>
{/if}
</div>
</svelte:element>
</a>
</li>
{/each}
</ol>
Expand Down
99 changes: 23 additions & 76 deletions web-admin/src/components/projects/dashboards.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { V1GetProjectResponse } from "@rilldata/web-admin/client";
import type { V1CatalogEntry } from "@rilldata/web-common/runtime-client";
import {
createRuntimeServiceListCatalogEntries,
createRuntimeServiceListFiles,
import type {
V1CatalogEntry,
V1MetricsView,
} from "@rilldata/web-common/runtime-client";
import { createRuntimeServiceListCatalogEntries } from "@rilldata/web-common/runtime-client";
import Axios from "axios";
import { Readable, derived } from "svelte/store";

export interface DashboardListItem {
name: string;
Expand All @@ -16,7 +15,7 @@ export interface DashboardListItem {

export async function getDashboardsForProject(
projectData: V1GetProjectResponse
): Promise<DashboardListItem[]> {
): Promise<V1MetricsView[]> {
// There may not be a prodDeployment if the project was hibernated
if (!projectData.prodDeployment) {
return [];
Expand All @@ -35,84 +34,32 @@ export async function getDashboardsForProject(
},
});

// get all valid and invalid dashboards
const filesRequest = axios.get(
`/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/files?glob=dashboards/*.yaml`
);

// get the valid dashboards
const catalogEntriesRequest = axios.get(
const catalogEntriesResponse = await axios.get(
`/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/catalog?type=OBJECT_TYPE_METRICS_VIEW`
);

const [filesResponse, catalogEntriesResponse] = await Promise.all([
filesRequest,
catalogEntriesRequest,
]);

const filePaths = filesResponse.data?.paths;
const catalogEntries = catalogEntriesResponse.data?.entries;
const catalogEntries = catalogEntriesResponse.data
?.entries as V1CatalogEntry[];

// compose the dashboard list items
const dashboardListItems = getDashboardListItemsFromFilesAndCatalogEntries(
filePaths,
catalogEntries
const dashboards = catalogEntries?.map(
(entry: V1CatalogEntry) => entry.metricsView
);

return dashboardListItems;
return dashboards;
}

export function getDashboardListItemsFromFilesAndCatalogEntries(
filePaths: string[],
catalogEntries: V1CatalogEntry[]
): DashboardListItem[] {
const dashboardListings = filePaths?.map((path: string) => {
const name = path.replace("/dashboards/", "").replace(".yaml", "");
const catalogEntry = catalogEntries?.find(
(entry: V1CatalogEntry) => entry.path === path
);
const title = catalogEntry?.metricsView?.label;
const description = catalogEntry?.metricsView?.description;
// invalid dashboards are not in the catalog
const isValid = !!catalogEntry;
return {
name,
title,
description,
isValid,
};
});

return dashboardListings;
}

export function useDashboardListItems(instanceId: string): Readable<{
items: DashboardListItem[];
isSuccess: boolean;
}> {
return derived(
[
createRuntimeServiceListFiles(instanceId, {
glob: "dashboards/*.yaml",
}),
createRuntimeServiceListCatalogEntries(instanceId, {
type: "OBJECT_TYPE_METRICS_VIEW",
}),
],
([dashboardFiles, dashboardCatalogEntries]) => {
if (!dashboardFiles.isSuccess || !dashboardCatalogEntries.isSuccess)
return {
isSuccess: false,
items: [],
};

return {
isSuccess: true,
items: getDashboardListItemsFromFilesAndCatalogEntries(
dashboardFiles?.data?.paths ?? [],
dashboardCatalogEntries?.data?.entries ?? []
),
};
export function useDashboards(instanceId: string) {
return createRuntimeServiceListCatalogEntries(
instanceId,
{
type: "OBJECT_TYPE_METRICS_VIEW",
},
{
query: {
select: (data) => {
return data.entries.map((entry) => entry.metricsView);
},
},
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@
createAdminServiceGetProject,
V1DeploymentStatus,
} from "@rilldata/web-admin/client";
import {
getDashboardsForProject,
useDashboardListItems,
} from "@rilldata/web-admin/components/projects/dashboards";
import { getDashboardsForProject } from "@rilldata/web-admin/components/projects/dashboards";
import { invalidateDashboardsQueries } from "@rilldata/web-admin/components/projects/invalidations";
import { useProjectDeploymentStatus } from "@rilldata/web-admin/components/projects/selectors";
import { Dashboard } from "@rilldata/web-common/features/dashboards";
import DashboardStateProvider from "@rilldata/web-common/features/dashboards/DashboardStateProvider.svelte";
import DashboardURLStateProvider from "@rilldata/web-common/features/dashboards/proto-state/DashboardURLStateProvider.svelte";
import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte";
import {
getRuntimeServiceListCatalogEntriesQueryKey,
getRuntimeServiceListFilesQueryKey,
createRuntimeServiceGetCatalogEntry,
getRuntimeServiceGetCatalogEntryQueryKey,
} from "@rilldata/web-common/runtime-client";
import type { QueryError } from "@rilldata/web-common/runtime-client/error";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { useQueryClient } from "@tanstack/svelte-query";
import { errorStore } from "../../../../components/errors/error-store";
import ProjectBuilding from "../../../../components/projects/ProjectBuilding.svelte";
import ProjectErrored from "../../../../components/projects/ProjectErrored.svelte";

const queryClient = useQueryClient();

Expand All @@ -44,6 +41,8 @@
$: isProjectErrored =
$projectDeploymentStatus.data ===
V1DeploymentStatus.DEPLOYMENT_STATUS_ERROR;
$: isProjectBuilding = isProjectPending || isProjectReconciling;
$: isProjectBuilt = isProjectOK || isProjectErrored;

let isProjectOK: boolean;

Expand All @@ -56,16 +55,9 @@
if (projectWasNotOk && isProjectOK) {
getDashboardsAndInvalidate();

// Invalidate the queries used to assess dashboard validity
queryClient.invalidateQueries(
getRuntimeServiceListFilesQueryKey(instanceId, {
glob: "dashboards/*.yaml",
})
);
// Invalidate the query used to assess dashboard validity
queryClient.invalidateQueries(
getRuntimeServiceListCatalogEntriesQueryKey(instanceId, {
type: "OBJECT_TYPE_METRICS_VIEW",
})
getRuntimeServiceGetCatalogEntryQueryKey(instanceId, dashboardName)
);
}
}
Expand All @@ -76,21 +68,19 @@
return invalidateDashboardsQueries(queryClient, dashboardNames);
}

// We avoid calling `GetCatalogEntry` to check for dashboard validity because that would trigger a 404 page.
$: dashboardListItems = useDashboardListItems(instanceId);
$: currentDashboard = $dashboardListItems?.items?.find(
(listing) => listing.name === $page.params.dashboard
);
$: isDashboardOK = currentDashboard?.isValid;
$: isDashboardErrored = !!currentDashboard && !currentDashboard.isValid;
$: isDashboardNotFound = $dashboardListItems?.isSuccess && !currentDashboard;
$: dashboard = createRuntimeServiceGetCatalogEntry(instanceId, dashboardName);
$: isDashboardOK = $dashboard.isSuccess;
$: isDashboardNotFound =
$dashboard.isError &&
($dashboard.error as QueryError)?.response?.status === 400;
// isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile

// If no dashboard is found, show a 404 page
if ((isProjectOK || isProjectErrored) && isDashboardNotFound) {
$: if (isProjectBuilt && isDashboardNotFound) {
errorStore.set({
statusCode: 404,
header: "Dashboard not found",
body: `The dashboard you requested could not be found. Please check that you have provided a valid dashboard name.`,
body: `The dashboard you requested could not be found. Please check that you provided the name of a working dashboard.`,
});
}
</script>
Expand All @@ -102,7 +92,7 @@
<!-- Note: Project and dashboard states might appear to diverge. A project could be errored
because dashboard #1 is errored, but dashboard #2 could be OK. -->

{#if isProjectPending || (isProjectReconciling && isDashboardNotFound)}
{#if isProjectBuilding && isDashboardNotFound}
<ProjectBuilding organization={orgName} project={projectName} />
{:else if isDashboardOK}
<StateManagersProvider metricsViewName={dashboardName}>
Expand All @@ -114,6 +104,8 @@
</DashboardStateProvider>
{/key}
</StateManagersProvider>
{:else if isDashboardErrored}
<ProjectErrored organization={orgName} project={projectName} />
{/if}
<!-- We'll reinstate this case once we integrate the new Reconcile -->
<!-- {:else if isDashboardErrored}
<ProjectErrored organization={orgName} project={projectName} />
{/if} -->
Loading