From 6f357950cb913dc4a790504089c530fc2ce2c117 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:30:59 -0500 Subject: [PATCH 1/3] general settings --- .../src/features/projects/ProjectTabs.svelte | 4 +- .../projects/settings/DeleteProject.svelte | 60 +++++++ .../projects/settings/HibernateProject.svelte | 55 ++++++ .../settings/ProjectNameSettings.svelte | 170 ++++++++++++++++++ .../settings/PublicVisibilitySettings.svelte | 117 ++++++++++++ .../src/features/projects/settings/errors.ts | 13 ++ .../[project]/-/settings/+layout.svelte | 5 + .../[project]/-/settings/+page.svelte | 38 +++- 8 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 web-admin/src/features/projects/settings/DeleteProject.svelte create mode 100644 web-admin/src/features/projects/settings/HibernateProject.svelte create mode 100644 web-admin/src/features/projects/settings/ProjectNameSettings.svelte create mode 100644 web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte create mode 100644 web-admin/src/features/projects/settings/errors.ts diff --git a/web-admin/src/features/projects/ProjectTabs.svelte b/web-admin/src/features/projects/ProjectTabs.svelte index a58b490823b..b9c32e3c9c0 100644 --- a/web-admin/src/features/projects/ProjectTabs.svelte +++ b/web-admin/src/features/projects/ProjectTabs.svelte @@ -46,9 +46,7 @@ hasPermission: projectPermissions.manageProject, }, { - // TODO: Change this back to `/${organization}/${project}/-/settings` - // Once project settings are implemented - route: `/${organization}/${project}/-/settings/environment-variables`, + route: `/${organization}/${project}/-/settings`, label: "Settings", hasPermission: projectPermissions.manageProject, }, diff --git a/web-admin/src/features/projects/settings/DeleteProject.svelte b/web-admin/src/features/projects/settings/DeleteProject.svelte new file mode 100644 index 00000000000..e9a18d1af98 --- /dev/null +++ b/web-admin/src/features/projects/settings/DeleteProject.svelte @@ -0,0 +1,60 @@ + + + + + Permanently remove all contents of this project. This action cannot be + undone. + + + + + Delete project + + + diff --git a/web-admin/src/features/projects/settings/HibernateProject.svelte b/web-admin/src/features/projects/settings/HibernateProject.svelte new file mode 100644 index 00000000000..2f1d46ae69f --- /dev/null +++ b/web-admin/src/features/projects/settings/HibernateProject.svelte @@ -0,0 +1,55 @@ + + + + + Put this project into hibernation mode. Hibernated projects are paused and + do not consume resources. The project can be woken up at any time by + accessing it. + + + + + Hibernate project + + + diff --git a/web-admin/src/features/projects/settings/ProjectNameSettings.svelte b/web-admin/src/features/projects/settings/ProjectNameSettings.svelte new file mode 100644 index 00000000000..a89092820cc --- /dev/null +++ b/web-admin/src/features/projects/settings/ProjectNameSettings.svelte @@ -0,0 +1,170 @@ + + + + + + {#if $form.name && sanitizeProjectName($form.name) !== project} + + Renaming this project will invalidate all existing URLs and shared + links. + + {/if} + + + {#if error?.message} + + {error.message} + + {/if} + + Save + + + + diff --git a/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte b/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte new file mode 100644 index 00000000000..3d2b7bf7a94 --- /dev/null +++ b/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte @@ -0,0 +1,117 @@ + + + + + + + {#if isPublic} + This project is currently public. Anyone with the URL + can view this project. + {:else} + This project is currently private. Only members of + the organization can access this project. + {/if} + + + + + + {#if isPublic} + + Make private + + {:else} + + + Make public + + + {/if} + + diff --git a/web-admin/src/features/projects/settings/errors.ts b/web-admin/src/features/projects/settings/errors.ts new file mode 100644 index 00000000000..afc09e32ee5 --- /dev/null +++ b/web-admin/src/features/projects/settings/errors.ts @@ -0,0 +1,13 @@ +import type { RpcStatus } from "@rilldata/web-admin/client"; +import type { AxiosError } from "axios"; + +export function parseUpdateProjectError(err: AxiosError | null) { + if (!err) return {}; + + const message = err.response?.data?.message ?? err.message; + + return { + duplicateProject: message?.includes("already exists"), + message, + }; +} diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/+layout.svelte index c6a0aba8a10..ebf5d2698fd 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/+layout.svelte @@ -10,6 +10,11 @@ $: basePage = `/${organization}/${project}/-/settings`; const navItems = [ + { + label: "General", + route: "", + hasPermission: true, + }, { label: "Environment Variables", route: "/environment-variables", diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte index 183d98ab41d..449e886de05 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte @@ -1 +1,37 @@ - + + + + + + + Danger zone + + + + + + + + + From 1caf6807debbce8c57a29a877dc17986d30b5683 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:58:02 -0500 Subject: [PATCH 2/3] nit --- .../projects/settings/HibernateProject.svelte | 6 ++-- .../settings/PublicVisibilitySettings.svelte | 4 ++- .../[project]/-/settings/+page.svelte | 2 +- .../src/components/button/Button.svelte | 30 +++++++++++++++++-- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/web-admin/src/features/projects/settings/HibernateProject.svelte b/web-admin/src/features/projects/settings/HibernateProject.svelte index 2f1d46ae69f..2d296e6e1e6 100644 --- a/web-admin/src/features/projects/settings/HibernateProject.svelte +++ b/web-admin/src/features/projects/settings/HibernateProject.svelte @@ -48,8 +48,10 @@ error={hibernateResult.error?.message ?? ""} onConfirm={hibernateProject} > - - Hibernate project + + Hibernate project diff --git a/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte b/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte index 3d2b7bf7a94..34146b98517 100644 --- a/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte +++ b/web-admin/src/features/projects/settings/PublicVisibilitySettings.svelte @@ -109,7 +109,9 @@ onConfirm={makePublic} > - Make public + Make public {/if} diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte index 449e886de05..dcecbe30531 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/+page.svelte @@ -28,7 +28,7 @@ } .danger-zone-title { - @apply text-lg font-semibold text-red-600; + @apply text-lg font-semibold; } .danger-zone-items { diff --git a/web-common/src/components/button/Button.svelte b/web-common/src/components/button/Button.svelte index 921a31aa618..0c0b17c1262 100644 --- a/web-common/src/components/button/Button.svelte +++ b/web-common/src/components/button/Button.svelte @@ -2,6 +2,7 @@ export type ButtonType = | "primary" | "secondary" + | "secondary-destructive" | "tertiary" | "neutral" | "destructive" @@ -117,11 +118,17 @@ @apply cursor-not-allowed; } - /* button:focus { + button:focus, + a:focus { + @apply outline-none; + } + + button:focus-visible, + a:focus-visible { @apply outline-none; box-shadow: 0px 0px 4px 1px color-mix(in oklab, var(--focus-color) 50%, transparent); - } */ + } /* PRIMARY STYLES */ @@ -170,6 +177,25 @@ @apply opacity-50; } + /* SECONDARY DESTRUCTIVE STYLES */ + + .secondary-destructive { + --focus-color: var(--color-destructive-600); + @apply bg-transparent border border-destructive text-destructive; + } + + :global(.dark) .secondary-destructive { + @apply bg-transparent; + } + + .secondary-destructive:hover:not(:disabled) { + @apply bg-destructive/10 text-destructive; + } + + .secondary-destructive:disabled { + @apply opacity-50; + } + /* GHOST STYLES */ .ghost { From c8b0bbbd1bca4ec8925312e14384ce00e31fdb06 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:33:57 -0500 Subject: [PATCH 3/3] e2e --- web-admin/tests/projects.spec.ts | 173 +++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/web-admin/tests/projects.spec.ts b/web-admin/tests/projects.spec.ts index 75a24e526f2..cd7f096bd25 100644 --- a/web-admin/tests/projects.spec.ts +++ b/web-admin/tests/projects.spec.ts @@ -1,5 +1,10 @@ import { expect } from "@playwright/test"; import { test } from "./setup/base"; +import { + RILL_ORG_NAME, + RILL_PROJECT_NAME, + RILL_PROJECT_DISPLAY_NAME, +} from "./setup/constants"; test.describe("Projects", () => { test("admins should see the admin-only pages", async ({ adminPage }) => { @@ -9,4 +14,172 @@ test.describe("Projects", () => { adminPage.getByRole("link", { name: "Settings" }), ).toBeVisible(); }); + + test.describe("Settings", () => { + const settingsUrl = `/${RILL_ORG_NAME}/${RILL_PROJECT_NAME}/-/settings`; + + test("should display the settings page with all sections", async ({ + adminPage, + }) => { + await adminPage.goto(settingsUrl); + + // Check that the page title sections are visible + await expect( + adminPage.getByText("Project", { exact: true }), + ).toBeVisible(); + await expect(adminPage.getByText("Danger zone")).toBeVisible(); + + // Check that all settings sections are visible + await expect(adminPage.getByText("Public visibility")).toBeVisible(); + await expect(adminPage.getByText("Hibernate project")).toBeVisible(); + await expect(adminPage.getByText("Delete project")).toBeVisible(); + }); + + test("should display current project name", async ({ adminPage }) => { + await adminPage.goto(settingsUrl); + + const nameInput = adminPage.locator("#name"); + await expect(nameInput).toHaveValue(RILL_PROJECT_DISPLAY_NAME); + }); + + test("should show warning when renaming project", async ({ adminPage }) => { + await adminPage.goto(settingsUrl); + + const nameInput = adminPage.locator("#name"); + await nameInput.fill("Test Project Rename"); + + await expect( + adminPage.getByText( + "Renaming this project will invalidate all existing URLs and shared links.", + ), + ).toBeVisible(); + }); + + test("should enable Save button when changes are made", async ({ + adminPage, + }) => { + await adminPage.goto(settingsUrl); + + const saveButton = adminPage.getByRole("button", { name: "Save" }); + await expect(saveButton).toBeDisabled(); + + const descriptionInput = adminPage.locator("#description"); + await descriptionInput.fill("Test description change"); + + await expect(saveButton).toBeEnabled(); + }); + + test("should update project description successfully", async ({ + adminPage, + }) => { + await adminPage.goto(settingsUrl); + + const descriptionInput = adminPage.locator("#description"); + const saveButton = adminPage.getByRole("button", { name: "Save" }); + + const originalDescription = await descriptionInput.inputValue(); + + const testDescription = `E2E test description - ${Date.now()}`; + await descriptionInput.fill(testDescription); + await saveButton.click(); + + await expect(adminPage.getByLabel("Notification")).toHaveText( + "Updated project", + ); + + // Restore the original description + await descriptionInput.fill(originalDescription); + await saveButton.click(); + + await expect(adminPage.getByLabel("Notification")).toHaveText( + "Updated project", + ); + }); + + test("should display current visibility status", async ({ adminPage }) => { + await adminPage.goto(settingsUrl); + + const isPublicText = adminPage.getByText("This project is currently", { + exact: false, + }); + await expect(isPublicText).toBeVisible(); + }); + + test("should show confirmation dialog when making project public", async ({ + adminPage, + }) => { + await adminPage.goto(settingsUrl); + + const makePublicButton = adminPage.getByRole("button", { + name: "Make public", + }); + + // Only run if project is private + if (await makePublicButton.isVisible()) { + await makePublicButton.click(); + + await expect( + adminPage.getByText("Make this project public?"), + ).toBeVisible(); + + await adminPage.getByRole("button", { name: "Cancel" }).click(); + } + }); + + test("should show hibernate confirmation dialog", async ({ adminPage }) => { + await adminPage.goto(settingsUrl); + + await adminPage + .getByRole("button", { name: "Hibernate project" }) + .click(); + + await expect( + adminPage.getByText("Hibernate this project?"), + ).toBeVisible(); + + await expect( + adminPage.getByText(`Type hibernate ${RILL_PROJECT_NAME}`), + ).toBeVisible(); + + await expect( + adminPage.getByRole("button", { name: "Continue" }), + ).toBeDisabled(); + + // Enter confirmation text + const confirmInput = adminPage.locator("#confirmation"); + await confirmInput.fill(`hibernate ${RILL_PROJECT_NAME}`); + + await expect( + adminPage.getByRole("button", { name: "Continue" }), + ).toBeEnabled(); + + await adminPage.getByRole("button", { name: "Cancel" }).click(); + }); + + test("should show delete confirmation dialog", async ({ adminPage }) => { + await adminPage.goto(settingsUrl); + + await adminPage.getByRole("button", { name: "Delete project" }).click(); + + await expect(adminPage.getByText("Delete this project?")).toBeVisible(); + + await expect( + adminPage.getByText(`Type delete ${RILL_PROJECT_NAME}`), + ).toBeVisible(); + + await expect( + adminPage.getByRole("button", { name: "Continue" }), + ).toBeDisabled(); + + // Enter confirmation text + const confirmInput = adminPage.locator("#confirmation"); + await confirmInput.fill(`delete ${RILL_PROJECT_NAME}`); + + await expect( + adminPage.getByRole("button", { name: "Continue" }), + ).toBeEnabled(); + + await adminPage.getByRole("button", { name: "Cancel" }).click(); + }); + }); });
+ {#if isPublic} + This project is currently public. Anyone with the URL + can view this project. + {:else} + This project is currently private. Only members of + the organization can access this project. + {/if} +