diff --git a/web-admin/src/features/projects/status/resource-table/ActionsCell.svelte b/web-admin/src/features/projects/status/resource-table/ActionsCell.svelte index 2200f894768..94088d97413 100644 --- a/web-admin/src/features/projects/status/resource-table/ActionsCell.svelte +++ b/web-admin/src/features/projects/status/resource-table/ActionsCell.svelte @@ -5,6 +5,7 @@ import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; + import { refreshableTypes } from "@rilldata/web-common/features/resources/resource-filter-utils"; import type { V1Resource } from "@rilldata/web-common/runtime-client"; import { RefreshCcwIcon, @@ -23,7 +24,7 @@ export let onClickRefreshDialog: ( resourceName: string, resourceKind: string, - refreshType: "full" | "incremental", + refreshType: "refresh" | "full" | "incremental", ) => void; export let onClickRefreshErroredPartitions: (resourceName: string) => void; export let onClickViewSpec: ( @@ -39,8 +40,7 @@ export let onDropdownOpenChange: (isOpen: boolean) => void; $: isModel = resourceKind === ResourceKind.Model; - $: isSource = resourceKind === ResourceKind.Source; - $: canRefresh = isModel || isSource; + $: canRefresh = refreshableTypes.includes(resourceKind as ResourceKind); $: actions = isModel ? getAvailableModelActions(resource) : []; $: isPartitioned = actions.includes("viewPartitions"); @@ -93,8 +93,7 @@ {/if} - - {#if canRefresh} + {#if isModel} @@ -153,6 +152,24 @@ > {/if} + {:else if canRefresh} + + + + + + onClickRefreshDialog(resourceName, resourceKind, "refresh")} + > +
+ + {m.status_action_refresh()} +
+
+ {refreshTooltip} +
{/if} diff --git a/web-admin/src/features/projects/status/resource-table/ProjectResourcesTable.svelte b/web-admin/src/features/projects/status/resource-table/ProjectResourcesTable.svelte index 9fa8ec70283..1b438cca787 100644 --- a/web-admin/src/features/projects/status/resource-table/ProjectResourcesTable.svelte +++ b/web-admin/src/features/projects/status/resource-table/ProjectResourcesTable.svelte @@ -35,7 +35,7 @@ let isConfirmDialogOpen = false; let dialogResourceName = ""; let dialogResourceKind = ""; - let dialogRefreshType: "full" | "incremental" = "full"; + let dialogRefreshType: "refresh" | "full" | "incremental" = "refresh"; let isSpecDialogOpen = false; let specResourceName = ""; @@ -61,7 +61,7 @@ const openRefreshDialog = ( resourceName: string, resourceKind: string, - refreshType: "full" | "incremental", + refreshType: "refresh" | "full" | "incremental", ) => { dialogResourceName = resourceName; dialogResourceKind = resourceKind; diff --git a/web-admin/tests/project-status-refresh.spec.ts b/web-admin/tests/project-status-refresh.spec.ts index 1bf69cea68c..cd2dd6e9d74 100644 --- a/web-admin/tests/project-status-refresh.spec.ts +++ b/web-admin/tests/project-status-refresh.spec.ts @@ -37,7 +37,9 @@ test.describe("Project Status - Resource Refresh (openrtb)", () => { test("should show Full Refresh option for sources", async ({ adminPage }) => { // Wait for the source row to be visible before interacting - // Look for bids_data_raw source which should be in the openrtb test project + // Look for bids_data_raw source which should be in the openrtb test project. + // NOTE: `type: source` files are parsed into models (defined_as_source), so + // this row is a model and shows the full/incremental refresh actions. await expect(adminPage.getByText("bids_data_raw")).toBeVisible({ timeout: 60_000, }); @@ -61,6 +63,46 @@ test.describe("Project Status - Resource Refresh (openrtb)", () => { ).not.toBeVisible(); }); + test("should show only Refresh for metrics views", async ({ adminPage }) => { + await expect(adminPage.getByText("auction_metrics")).toBeVisible({ + timeout: 60_000, + }); + + const metricsViewRow = adminPage.locator(".row").filter({ + hasText: "auction_metrics", + }); + await metricsViewRow + .getByRole("button", { name: "Open resource actions" }) + .click(); + + await expect( + adminPage.getByRole("menuitem", { name: "Refresh", exact: true }), + ).toBeVisible(); + await expect( + adminPage.getByRole("menuitem", { name: "Full Refresh" }), + ).not.toBeVisible(); + await expect( + adminPage.getByRole("menuitem", { name: "Incremental Refresh" }), + ).not.toBeVisible(); + + await adminPage + .getByRole("menuitem", { name: "Refresh", exact: true }) + .click(); + await expect(adminPage.getByRole("alertdialog")).toBeVisible(); + await expect( + adminPage.getByRole("heading", { name: /Refresh auction_metrics/ }), + ).toBeVisible(); + await expect( + adminPage.getByText( + "Refreshing this resource will update all dependent resources.", + ), + ).toBeVisible(); + await expect( + adminPage.getByText(/Warning.*re-ingest ALL data/), + ).toHaveCount(0); + await adminPage.getByRole("button", { name: "Cancel" }).click(); + }); + test("should not show Incremental Refresh for non-incremental models", async ({ adminPage, }) => { diff --git a/web-common/src/features/projects/status/ActionsCell.svelte b/web-common/src/features/projects/status/ActionsCell.svelte index 5a7bf284b08..3a436a4bed6 100644 --- a/web-common/src/features/projects/status/ActionsCell.svelte +++ b/web-common/src/features/projects/status/ActionsCell.svelte @@ -29,32 +29,33 @@ $: isLoading = $triggerMutation.isPending; + $: isModel = resourceKind === ResourceKind.Model; $: supportsIncremental = - resourceKind === ResourceKind.Model && - resource?.model?.spec?.incremental === true; + isModel && resource?.model?.spec?.incremental === true; - async function handleRefresh(refreshType: "full" | "incremental") { + async function handleRefresh( + refreshType: "refresh" | "full" | "incremental", + ) { if (isLoading) return; try { - const body = - resourceKind === ResourceKind.Model - ? { - models: [ - { - model: resourceName, - full: refreshType === "full", - }, - ], - } - : { - resources: [ - { - kind: resourceKind, - name: resourceName, - }, - ], - }; + const body = isModel + ? { + models: [ + { + model: resourceName, + full: refreshType === "full", + }, + ], + } + : { + resources: [ + { + kind: resourceKind, + name: resourceName, + }, + ], + }; await $triggerMutation.mutateAsync(body); @@ -89,7 +90,7 @@ class="font-normal flex items-center" disabled={isLoading} onclick={() => { - handleRefresh("full"); + handleRefresh(isModel ? "full" : "refresh"); }} >
@@ -97,7 +98,9 @@ {isLoading ? m.status_refreshing() - : m.status_action_full_refresh()}
diff --git a/web-common/src/features/projects/status/RefreshResourceConfirmDialog.svelte b/web-common/src/features/projects/status/RefreshResourceConfirmDialog.svelte index 3a335df794c..4a55be35e6f 100644 --- a/web-common/src/features/projects/status/RefreshResourceConfirmDialog.svelte +++ b/web-common/src/features/projects/status/RefreshResourceConfirmDialog.svelte @@ -13,7 +13,7 @@ export let open = false; export let name: string; export let onRefresh: () => Promise | void; - export let refreshType: "full" | "incremental" = "full"; + export let refreshType: "refresh" | "full" | "incremental" = "full"; let isRefreshing = false; @@ -45,15 +45,19 @@ {refreshType === "full" ? m.status_action_full_refresh() - : m.status_action_incremental_refresh()} + : refreshType === "incremental" + ? m.status_action_incremental_refresh() + : m.status_action_refresh()} {truncateName(name)}?
{#if refreshType === "full"} {m.status_full_refresh_warning()} - {:else} + {:else if refreshType === "incremental"} {m.status_incremental_refresh_description()} + {:else} + {m.status_refresh_description()} {/if}
diff --git a/web-common/src/features/resources/ResourcesFilterableTable.svelte b/web-common/src/features/resources/ResourcesFilterableTable.svelte index b01eb6d1668..8a65888d87a 100644 --- a/web-common/src/features/resources/ResourcesFilterableTable.svelte +++ b/web-common/src/features/resources/ResourcesFilterableTable.svelte @@ -17,6 +17,7 @@ filterableTypes, filterResources, getStatusPriority, + refreshableTypes, statusFilters, } from "@rilldata/web-common/features/resources/resource-filter-utils"; import ActionsCell from "@rilldata/web-common/features/projects/status/ActionsCell.svelte"; @@ -194,8 +195,9 @@ resourceName: row.original.meta?.name?.name ?? "", canRefresh: !isRowReconciling && - (row.original.meta?.name?.kind === ResourceKind.Model || - row.original.meta?.name?.kind === ResourceKind.Source), + refreshableTypes.includes( + row.original.meta?.name?.kind as ResourceKind, + ), resource: row.original, onRefresh: onRefetch, onDescribe: handleDescribe, diff --git a/web-common/src/features/resources/resource-filter-utils.ts b/web-common/src/features/resources/resource-filter-utils.ts index 92e515ebcd9..6cebe79ea9f 100644 --- a/web-common/src/features/resources/resource-filter-utils.ts +++ b/web-common/src/features/resources/resource-filter-utils.ts @@ -27,6 +27,19 @@ export const filterableTypes = [ ResourceKind.Connector, ]; +// Resource kinds that support a manual refresh trigger from the resource listing. +// Excludes Alert and Report: triggering those executes them and dispatches notifications to recipients. +export const refreshableTypes = [ + ResourceKind.Source, + ResourceKind.Model, + ResourceKind.MetricsView, + ResourceKind.Explore, + ResourceKind.Canvas, + ResourceKind.Theme, + ResourceKind.API, + ResourceKind.Connector, +]; + export function getResourceStatus(r: V1Resource): ResourceStatus { if (r.meta?.reconcileError) return "error"; const status = r.meta?.reconcileStatus; diff --git a/web-common/src/lib/i18n/messages/en.json b/web-common/src/lib/i18n/messages/en.json index 5df177470ab..99a55f47280 100644 --- a/web-common/src/lib/i18n/messages/en.json +++ b/web-common/src/lib/i18n/messages/en.json @@ -1653,6 +1653,7 @@ "status_action_describe": "Describe", "status_action_full_refresh": "Full Refresh", "status_action_incremental_refresh": "Incremental Refresh", + "status_action_refresh": "Refresh", "status_action_refresh_errored_partitions": "Refresh Errored Partitions", "status_action_view_logs": "View Logs", "status_action_view_partitions": "View Partitions", @@ -1795,6 +1796,7 @@ "status_refresh_all_confirm_title": "Refresh all sources and models?", "status_refresh_all_sources_models": "Refresh all sources and models", "status_refresh_errored_confirm_body": "This will re-execute all partitions that failed during their last run. The refresh will happen in the background.", + "status_refresh_description": "Refreshing this resource will update all dependent resources.", "status_refresh_errored_confirm_title": "Refresh Errored Partitions for {modelName}?", "status_refreshing": "Refreshing...", "status_resource_reconciling": "Resource is currently being reconciled", diff --git a/web-common/src/lib/i18n/messages/es.json b/web-common/src/lib/i18n/messages/es.json index 089440a67a0..0b92095c49b 100644 --- a/web-common/src/lib/i18n/messages/es.json +++ b/web-common/src/lib/i18n/messages/es.json @@ -1662,6 +1662,7 @@ "status_action_describe": "Describir", "status_action_full_refresh": "Actualización completa", "status_action_incremental_refresh": "Actualización incremental", + "status_action_refresh": "Actualizar", "status_action_refresh_errored_partitions": "Actualizar particiones con error", "status_action_view_logs": "Ver registros", "status_action_view_partitions": "Ver particiones", @@ -1804,6 +1805,7 @@ "status_refresh_all_confirm_title": "¿Actualizar todas las fuentes y modelos?", "status_refresh_all_sources_models": "Actualizar todas las fuentes y modelos", "status_refresh_errored_confirm_body": "Se reejecutarán todas las particiones que fallaron en su última ejecución. La actualización se realizará en segundo plano.", + "status_refresh_description": "Actualizar este recurso actualizará todos los recursos dependientes.", "status_refresh_errored_confirm_title": "¿Actualizar particiones con error de {modelName}?", "status_refreshing": "Actualizando...", "status_resource_reconciling": "El recurso se está reconciliando",