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",