-
-
-
-
+
diff --git a/web-admin/tests/project-status-refresh.spec.ts b/web-admin/tests/project-status-refresh.spec.ts
new file mode 100644
index 00000000000..e11b4f8ba6f
--- /dev/null
+++ b/web-admin/tests/project-status-refresh.spec.ts
@@ -0,0 +1,253 @@
+import { expect } from "@playwright/test";
+import { test } from "./setup/base";
+
+test.describe("Project Status - Resource Refresh (openrtb)", () => {
+ // Increase timeout for tests that interact with virtualized tables
+ test.setTimeout(60_000);
+
+ test.beforeEach(async ({ adminPage }) => {
+ // Navigate to the project status page
+ await adminPage.goto("/e2e/openrtb/-/status");
+ // Wait for the resources table to load with actual data
+ await expect(adminPage.getByText("Resources")).toBeVisible();
+ // Wait for a specific model name to appear (indicates data is loaded)
+ // Note: VirtualizedTable uses div.row elements, not role="row"
+ await expect(adminPage.getByText("auction_data_model")).toBeVisible({
+ timeout: 60_000,
+ });
+ });
+
+ test("should show Full Refresh option for models", async ({ adminPage }) => {
+ // Find the auction_data_model row and click its actions menu
+ // The row contains both the model name and a "Model" badge
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "auction_data_model",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Verify "Full Refresh" is visible
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Full Refresh" }),
+ ).toBeVisible();
+ });
+
+ 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
+ await expect(adminPage.getByText("bids_data_raw")).toBeVisible({
+ timeout: 60_000,
+ });
+
+ // Find a source row and click its actions menu
+ const sourceRow = adminPage.locator(".row").filter({
+ hasText: "bids_data_raw",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await sourceRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Verify "Full Refresh" is visible
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Full Refresh" }),
+ ).toBeVisible();
+
+ // Incremental Refresh should NOT be visible for sources
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Incremental Refresh" }),
+ ).not.toBeVisible();
+ });
+
+ test("should not show Incremental Refresh for non-incremental models", async ({
+ adminPage,
+ }) => {
+ // Find the auction_data_model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "auction_data_model",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Verify "Full Refresh" is visible
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Full Refresh" }),
+ ).toBeVisible();
+
+ // For non-incremental models, "Incremental Refresh" should not be visible
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Incremental Refresh" }),
+ ).not.toBeVisible();
+ });
+
+ test("should not show Refresh Errored Partitions for models without errors", async ({
+ adminPage,
+ }) => {
+ // Find the auction_data_model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "auction_data_model",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // "Refresh Errored Partitions" should not be visible for models without errored partitions
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Refresh Errored Partitions" }),
+ ).not.toBeVisible();
+ });
+
+ test("should show correct dialog for Full Refresh", async ({ adminPage }) => {
+ // Find the auction_data_model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "auction_data_model",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Click "Full Refresh"
+ await adminPage.getByRole("menuitem", { name: "Full Refresh" }).click();
+
+ // Verify the dialog shows "Full Refresh" in the title
+ await expect(adminPage.getByRole("alertdialog")).toBeVisible();
+ await expect(
+ adminPage.getByRole("heading", { name: /Full Refresh/ }),
+ ).toBeVisible();
+
+ // Verify the warning message about full refresh
+ await expect(
+ adminPage.getByText(/Warning.*will re-ingest ALL data from scratch/),
+ ).toBeVisible();
+
+ // Close dialog by clicking cancel
+ await adminPage.getByRole("button", { name: "Cancel" }).click();
+ await expect(adminPage.getByRole("alertdialog")).not.toBeVisible();
+ });
+});
+
+test.describe("Project Status - Incremental Model Refresh (incremental-test)", () => {
+ // Increase timeout for tests that interact with virtualized tables
+ test.setTimeout(60_000);
+
+ test.beforeEach(async ({ adminPage }) => {
+ // Navigate to the incremental-test project status page
+ await adminPage.goto("/e2e/incremental-test/-/status");
+ // Wait for the resources table to load with actual data
+ await expect(adminPage.getByText("Resources")).toBeVisible();
+ // Wait for the specific model rows to appear (indicates data is loaded)
+ // Note: VirtualizedTable uses div.row elements, not role="row"
+ await expect(adminPage.getByText("success_partition")).toBeVisible({
+ timeout: 60_000,
+ });
+ });
+
+ test("should show Incremental Refresh option for incremental models", async ({
+ adminPage,
+ }) => {
+ // Find the success_partition model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "success_partition",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Verify both "Full Refresh" and "Incremental Refresh" are visible
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Full Refresh" }),
+ ).toBeVisible();
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Incremental Refresh" }),
+ ).toBeVisible();
+ });
+
+ test("should show correct dialog for Incremental Refresh", async ({
+ adminPage,
+ }) => {
+ // Find the success_partition model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "success_partition",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Click "Incremental Refresh"
+ await adminPage
+ .getByRole("menuitem", { name: "Incremental Refresh" })
+ .click();
+
+ // Verify the dialog shows "Incremental Refresh" in the title
+ await expect(adminPage.getByRole("alertdialog")).toBeVisible();
+ await expect(
+ adminPage.getByRole("heading", { name: /Incremental Refresh/ }),
+ ).toBeVisible();
+
+ // Verify the message about updating dependent resources
+ await expect(
+ adminPage.getByText("will update all dependent resources"),
+ ).toBeVisible();
+
+ // Close dialog by clicking cancel
+ await adminPage.getByRole("button", { name: "Cancel" }).click();
+ await expect(adminPage.getByRole("alertdialog")).not.toBeVisible();
+ });
+
+ test("should show Refresh Errored Partitions for models with errored partitions", async ({
+ adminPage,
+ }) => {
+ // Find the failed_partition model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "failed_partition",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Verify "Refresh Errored Partitions" is visible for models with errors
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Refresh Errored Partitions" }),
+ ).toBeVisible();
+ });
+
+ test("should show correct dialog for Refresh Errored Partitions", async ({
+ adminPage,
+ }) => {
+ // Find the failed_partition model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "failed_partition",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // Click "Refresh Errored Partitions"
+ await adminPage
+ .getByRole("menuitem", { name: "Refresh Errored Partitions" })
+ .click();
+
+ // Verify the dialog shows "Refresh Errored Partitions" in the title
+ await expect(adminPage.getByRole("alertdialog")).toBeVisible();
+ await expect(
+ adminPage.getByRole("heading", { name: /Refresh Errored Partitions/ }),
+ ).toBeVisible();
+
+ // Verify the message about re-running failed partitions
+ await expect(
+ adminPage.getByText("re-run all partitions that failed"),
+ ).toBeVisible();
+
+ // Close dialog by clicking cancel
+ await adminPage.getByRole("button", { name: "Cancel" }).click();
+ await expect(adminPage.getByRole("alertdialog")).not.toBeVisible();
+ });
+
+ test("should not show Refresh Errored Partitions for successful incremental models", async ({
+ adminPage,
+ }) => {
+ // Find the success_partition model row and click its actions menu
+ const modelRow = adminPage.locator(".row").filter({
+ hasText: "success_partition",
+ });
+ // Target the dropdown menu trigger specifically (rows may have multiple buttons)
+ await modelRow.locator("[data-melt-dropdown-menu-trigger]").click();
+
+ // "Refresh Errored Partitions" should NOT be visible for models without errors
+ await expect(
+ adminPage.getByRole("menuitem", { name: "Refresh Errored Partitions" }),
+ ).not.toBeVisible();
+ });
+});
diff --git a/web-admin/tests/setup/setup.ts b/web-admin/tests/setup/setup.ts
index ce2ddd7dd07..c4bd104e8ca 100644
--- a/web-admin/tests/setup/setup.ts
+++ b/web-admin/tests/setup/setup.ts
@@ -325,4 +325,48 @@ setup.describe("global setup", () => {
adminPage.getByRole("link", { name: "Adbids dashboard" }),
).toBeVisible();
});
+
+ setup("should deploy the incremental-test project", async ({ adminPage }) => {
+ // Deploy the incremental-test project (for testing incremental model refresh)
+ const { match } = await spawnAndMatch(
+ "rill",
+ [
+ "deploy",
+ "--path",
+ "../web-common/tests/projects/incremental-test",
+ "--project",
+ "incremental-test",
+ "--archive",
+ "--interactive=false",
+ ],
+ /https?:\/\/[^\s]+/,
+ );
+
+ // Navigate to the project URL and expect to see the successful deployment
+ const url = match[0];
+ await adminPage.goto(url);
+ await expect(
+ adminPage.getByRole("link", { name: RILL_ORG_NAME }),
+ ).toBeVisible(); // Organization breadcrumb
+ await expect(
+ adminPage.getByRole("link", { name: "incremental-test", exact: true }),
+ ).toBeVisible(); // Project breadcrumb
+
+ // Expect to land on the project home page
+ await adminPage.waitForURL(`/${RILL_ORG_NAME}/incremental-test`);
+
+ // Wait for the project to be ready by checking the status page
+ await adminPage.goto(`/${RILL_ORG_NAME}/incremental-test/-/status`);
+ await expect(adminPage.getByText("Resources")).toBeVisible({
+ timeout: 60_000,
+ });
+
+ // Verify the incremental models are listed
+ await expect(adminPage.getByText("success_partition")).toBeVisible({
+ timeout: 30_000,
+ });
+ await expect(adminPage.getByText("failed_partition")).toBeVisible({
+ timeout: 30_000,
+ });
+ });
});
diff --git a/web-common/src/components/table/VirtualizedTable.svelte b/web-common/src/components/table/VirtualizedTable.svelte
index 28f72ddc6a1..a0201c1fcad 100644
--- a/web-common/src/components/table/VirtualizedTable.svelte
+++ b/web-common/src/components/table/VirtualizedTable.svelte
@@ -24,6 +24,7 @@
export let rowHeight = 46;
export let containerHeight = 400;
export let overscan = 1;
+ export let tableId: string | undefined = undefined;
let containerElement: HTMLDivElement;
let sorting: SortingState = [];
@@ -108,6 +109,7 @@
diff --git a/web-common/src/features/models/partitions/PartitionsTable.svelte b/web-common/src/features/models/partitions/PartitionsTable.svelte
index 5bd23df3cbe..bcd8b2f7629 100644
--- a/web-common/src/features/models/partitions/PartitionsTable.svelte
+++ b/web-common/src/features/models/partitions/PartitionsTable.svelte
@@ -135,6 +135,7 @@
flexRender(TriggerPartition, {
partitionKey: (row as Row).original
.key as string,
+ resource,
}),
},
]
diff --git a/web-common/src/features/models/partitions/TriggerPartition.svelte b/web-common/src/features/models/partitions/TriggerPartition.svelte
index 51b13cf3a45..8acef2221a1 100644
--- a/web-common/src/features/models/partitions/TriggerPartition.svelte
+++ b/web-common/src/features/models/partitions/TriggerPartition.svelte
@@ -1,10 +1,13 @@