Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
633cda1
Add Claude Code PM skills
ericokuma Feb 18, 2026
51a2490
feat(admin-console): add superuser auth guard for admin layout
ericokuma Mar 19, 2026
e7158a5
feat(admin-console): add page header component
ericokuma Mar 19, 2026
417e96b
feat(admin-console): add sidebar navigation component
ericokuma Mar 19, 2026
441d77d
feat(admin-console): add shared UI components (ConfirmDialog, StatusB…
ericokuma Mar 19, 2026
f8f3cc0
feat(admin-console): add admin layout with sidebar + content area
ericokuma Mar 19, 2026
3979190
feat(admin-console): add dashboard home page with quick action cards
ericokuma Mar 19, 2026
29a2915
feat(admin-console): add user management API selectors
ericokuma Mar 19, 2026
14647e1
feat(admin-console): add billing management API selectors
ericokuma Mar 19, 2026
f502d41
feat(admin-console): add user management page with search, assume, an…
ericokuma Mar 19, 2026
ccd6905
feat(admin-console): add billing management page with trial extension…
ericokuma Mar 19, 2026
efff27c
feat(admin-console): add admin link in top nav for superusers
ericokuma Mar 19, 2026
55affa1
feat(admin-console): add quota management selectors
ericokuma Mar 19, 2026
7223698
feat(admin-console): add organization management selectors
ericokuma Mar 19, 2026
5b7a419
feat(admin-console): add quota management page with editable fields
ericokuma Mar 19, 2026
61f2da3
feat(admin-console): add project management selectors
ericokuma Mar 19, 2026
c92255f
feat(admin-console): add domain whitelist management page
ericokuma Mar 19, 2026
2abf00e
feat(admin-console): add project management page with search, hiberna…
ericokuma Mar 19, 2026
b5561a8
feat(admin-console): add annotations management page
ericokuma Mar 19, 2026
0e76ab7
feat(admin-console): add superuser management page
ericokuma Mar 19, 2026
6d28008
feat(admin-console): add stub pages for virtual files and runtime man…
ericokuma Mar 19, 2026
33f445e
fix(admin-console): fix annotations page Svelte template compile error
ericokuma Mar 19, 2026
c81c0f6
Revert "Add Claude Code PM skills"
ericokuma Mar 20, 2026
61d9d36
fix(admin-console): fix type errors, scoped query invalidation, and r…
ericokuma Mar 20, 2026
e058d49
fix(admin-console): add missing new files and remove deleted pages
ericokuma Mar 21, 2026
f276684
fix(admin-console): sync sidebar, selectors with intended state
ericokuma Mar 21, 2026
1580e70
fix(admin-console): move admin link to profile menu, fix billing 500,…
ericokuma Mar 21, 2026
11f34d2
fix(admin-console): replace `@apply` PostCSS with inline Tailwind cla…
ericokuma Mar 21, 2026
8a48cab
fix(admin-console): add dark mode backgrounds and missing dark text v…
ericokuma Mar 21, 2026
64de7ce
fix(admin-console): remove dark: prefixes, use auto-adapting CSS vari…
ericokuma Mar 21, 2026
11d3911
fix(admin-console): remove billing customer ID and repair sections, a…
ericokuma Mar 23, 2026
111099f
fix(admin-console): show current prod slots when editing
ericokuma Mar 23, 2026
ce3b7f4
fix(admin-console): add "New:" label before slots input
ericokuma Mar 23, 2026
e78e6b6
refactor(superuser-console): address code review feedback
ericokuma Mar 24, 2026
79cb89e
feat(superuser-console): add keyboard navigation to `OrgPicker` dropdown
ericokuma Mar 24, 2026
fec80ff
Merge remote-tracking branch 'origin/main' into eokuma/admin-console
ericokuma Mar 24, 2026
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
27 changes: 26 additions & 1 deletion web-admin/src/features/authentication/AvatarButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,32 @@
type UserLike,
} from "@rilldata/web-common/features/help/initPylonChat";
import { posthogIdentify } from "@rilldata/web-common/lib/analytics/posthog";
import { createAdminServiceGetCurrentUser } from "../../client";
import {
createAdminServiceGetCurrentUser,
createAdminServiceListSuperusers,
} from "../../client";
import ProjectAccessControls from "../projects/ProjectAccessControls.svelte";
import ViewAsUserPopover from "../view-as-user/ViewAsUserPopover.svelte";
import ThemeToggle from "@rilldata/web-common/features/themes/ThemeToggle.svelte";

const user = createAdminServiceGetCurrentUser();
// Fire ListSuperusers once per session to check if the avatar menu should
// show the "Superuser Console" link. Non-superusers get a single 403 that
// TanStack Query silently caches as an error (retry: false, staleTime: Infinity
// ensures no repeated requests across component remounts).
const superusers = createAdminServiceListSuperusers({
query: {
enabled: !!$user.data?.user?.email,
retry: false,
staleTime: Infinity,
},
});
$: isSuperuser =
$superusers.isSuccess &&
!!$user.data?.user?.email &&
($superusers.data?.users ?? []).some(
(su) => su.email === $user.data?.user?.email,
);

let imgContainer: HTMLElement;
let primaryMenuOpen = false;
Expand Down Expand Up @@ -128,6 +148,11 @@
{/if}
{/if}

{#if isSuperuser}
<DropdownMenu.Item href="/-/superuser">Superuser Console</DropdownMenu.Item>
<DropdownMenu.Separator />
{/if}

<ThemeToggle />
<DropdownMenu.Separator />

Expand Down
31 changes: 31 additions & 0 deletions web-admin/src/features/superuser/billing/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Billing-related queries and mutations for the superuser console
import {
adminServiceGetPaymentsPortalURL,
createAdminServiceSudoExtendTrial,
createAdminServiceSudoDeleteOrganizationBillingIssue,
createAdminServiceListOrganizationBillingIssues,
} from "@rilldata/web-admin/client";

export async function getBillingSetupURL(org: string): Promise<string> {
const resp = await adminServiceGetPaymentsPortalURL(org, {
setup: true,
superuserForceAccess: true,
});
return resp.url ?? "";
}

export function createExtendTrialMutation() {
return createAdminServiceSudoExtendTrial();
}

export function createDeleteBillingIssueMutation() {
return createAdminServiceSudoDeleteOrganizationBillingIssue();
}

export function getBillingIssues(org: string) {
return createAdminServiceListOrganizationBillingIssues(
org,
{ superuserForceAccess: true },
{ query: { enabled: org.length > 0 } },
);
}
15 changes: 15 additions & 0 deletions web-admin/src/features/superuser/layout/SuperuserPageHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
export let title: string;
export let description: string = "";
</script>

<div class="mb-6">
<h1 class="text-xl font-semibold text-fg-primary">
{title}
</h1>
{#if description}
<p class="text-sm text-fg-secondary mt-1">
{description}
</p>
{/if}
</div>
51 changes: 51 additions & 0 deletions web-admin/src/features/superuser/layout/SuperuserSidebar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script lang="ts">
import LeftNav from "@rilldata/web-admin/components/nav/LeftNav.svelte";

const basePage = "/-/superuser";
const baseRoute = "/-/superuser";

const navGroups = [
{
heading: "People",
items: [
{ label: "Users", route: "" },
{ label: "Superusers", route: "/superusers" },
],
},
{
heading: "Billing & Plans",
items: [
{ label: "Billing", route: "/billing" },
{ label: "Quotas", route: "/quotas" },
],
},
{
heading: "Resources",
items: [
{ label: "Organizations", route: "/organizations" },
{ label: "Projects", route: "/projects" },
],
},
];
</script>

<nav class="w-56 flex-shrink-0 border-r flex flex-col h-full">
<div class="px-4 py-4 border-b">
<span class="text-sm font-semibold text-fg-primary">
Superuser Console
</span>
</div>

<div class="flex-1 overflow-y-auto py-3 px-3">
{#each navGroups as group}
<div class="mb-4">
<span
class="text-sm font-semibold uppercase tracking-wider text-fg-muted px-2 mb-1 block"
>
{group.heading}
</span>
<LeftNav {basePage} {baseRoute} navItems={group.items} minWidth="auto" />
</div>
{/each}
</div>
</nav>
44 changes: 44 additions & 0 deletions web-admin/src/features/superuser/organizations/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Organization-related queries for the superuser console
import {
createAdminServiceGetOrganization,
createAdminServiceListOrganizationMemberUsers,
createAdminServiceListProjectsForOrganization,
createAdminServiceSearchProjectNames,
createAdminServiceDeleteOrganization,
} from "@rilldata/web-admin/client";

export function getOrganization(org: string) {
return createAdminServiceGetOrganization(
org,
{ superuserForceAccess: true },
{ query: { enabled: org.length > 0 } },
);
}

export function getOrgMembers(org: string) {
return createAdminServiceListOrganizationMemberUsers(
org,
{ superuserForceAccess: true },
{ query: { enabled: org.length > 0 } },
);
}

export function getOrgProjects(org: string) {
return createAdminServiceListProjectsForOrganization(
org,
{},
{ query: { enabled: org.length > 0 } },
);
}

export function createDeleteOrgMutation() {
return createAdminServiceDeleteOrganization();
}

// Search for org names by searching project paths (org/project) and extracting unique org names
export function searchOrgNames(query: string) {
return createAdminServiceSearchProjectNames(
{ namePattern: `%${query}%/%`, pageSize: 100 },
{ query: { enabled: query.length >= 3 } },
);
}
31 changes: 31 additions & 0 deletions web-admin/src/features/superuser/projects/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Project-related queries and mutations for the superuser console
import {
createAdminServiceSearchProjectNames,
createAdminServiceGetProject,
createAdminServiceUpdateProject,
createAdminServiceRedeployProject,
createAdminServiceHibernateProject,
} from "@rilldata/web-admin/client";

export function searchProjects(namePattern: string) {
return createAdminServiceSearchProjectNames(
{ namePattern: `%${namePattern}%`, pageSize: 50 },
{ query: { enabled: namePattern.length >= 3 } },
);
}

export function getProject(org: string, project: string) {
return createAdminServiceGetProject(org, project);
}

export function createUpdateProjectMutation() {
return createAdminServiceUpdateProject();
}

export function createRedeployProjectMutation() {
return createAdminServiceRedeployProject();
}

export function createHibernateProjectMutation() {
return createAdminServiceHibernateProject();
}
17 changes: 17 additions & 0 deletions web-admin/src/features/superuser/quotas/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Quota-related queries and mutations for the superuser console
import {
createAdminServiceGetOrganization,
createAdminServiceSudoUpdateOrganizationQuotas,
} from "@rilldata/web-admin/client";

export function getOrgForQuotas(org: string) {
return createAdminServiceGetOrganization(
org,
{ superuserForceAccess: true },
{ query: { enabled: org.length > 0 } },
);
}

export function createUpdateOrgQuotasMutation() {
return createAdminServiceSudoUpdateOrganizationQuotas();
}
98 changes: 98 additions & 0 deletions web-admin/src/features/superuser/shared/OrgPicker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script lang="ts">
import SearchInput from "./SearchInput.svelte";
import { searchOrgNames } from "@rilldata/web-admin/features/superuser/organizations/selectors";

export let value: string = "";
export let placeholder: string = "Organization name...";

let searchQuery = "";
let showResults = false;
let highlightedIndex = -1;

$: orgNamesQuery = searchOrgNames(searchQuery);
$: matchedOrgs = extractUniqueOrgs($orgNamesQuery.data?.names ?? []);
// Reset highlight when results change
$: matchedOrgs, (highlightedIndex = -1);

function extractUniqueOrgs(names: string[]): string[] {
const orgs = new Set<string>();
for (const name of names) {
const slash = name.indexOf("/");
if (slash > 0) orgs.add(name.substring(0, slash));
}
return [...orgs].sort();
}

function handleSearch(e: CustomEvent<string>) {
searchQuery = e.detail;
showResults = true;
// Clear selection when user changes the search text
if (value && searchQuery !== value) {
value = "";
}
}

function selectOrg(org: string) {
value = org;
searchQuery = org;
showResults = false;
highlightedIndex = -1;
}

function handleKeydown(e: CustomEvent<KeyboardEvent>) {
const key = e.detail.key;
if (!showResults || !matchedOrgs.length) return;

if (key === "ArrowDown") {
e.detail.preventDefault();
highlightedIndex = (highlightedIndex + 1) % matchedOrgs.length;
} else if (key === "ArrowUp") {
e.detail.preventDefault();
highlightedIndex =
highlightedIndex <= 0 ? matchedOrgs.length - 1 : highlightedIndex - 1;
} else if (key === "Enter" && highlightedIndex >= 0) {
e.detail.preventDefault();
selectOrg(matchedOrgs[highlightedIndex]);
} else if (key === "Escape") {
showResults = false;
highlightedIndex = -1;
}
}
</script>

<div class="relative">
<SearchInput
bind:value={searchQuery}
{placeholder}
on:search={handleSearch}
on:keydown={handleKeydown}
/>
{#if showResults && searchQuery.length >= 3 && !value}
{#if $orgNamesQuery.isFetching}
<div class="absolute z-10 left-0 right-0 mt-1 rounded-md border bg-surface-base shadow-md p-2">
<p class="text-sm text-fg-secondary">Searching...</p>
</div>
{:else if matchedOrgs.length > 0}
<div
class="absolute z-10 left-0 right-0 mt-1 flex flex-col gap-0.5 max-h-48 overflow-y-auto rounded-md border bg-surface-base shadow-md"
role="listbox"
>
{#each matchedOrgs as org, i}
<button
class="px-3 py-2 text-sm text-fg-primary text-left hover:bg-surface-hover cursor-pointer"
class:bg-surface-hover={i === highlightedIndex}
role="option"
aria-selected={i === highlightedIndex}
on:click={() => selectOrg(org)}
>
{org}
</button>
{/each}
</div>
{:else if $orgNamesQuery.isSuccess}
<div class="absolute z-10 left-0 right-0 mt-1 rounded-md border bg-surface-base shadow-md p-2">
<p class="text-sm text-fg-secondary">No organizations found</p>
</div>
{/if}
{/if}
</div>
37 changes: 37 additions & 0 deletions web-admin/src/features/superuser/shared/SearchInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";

export let placeholder: string = "Search...";
export let value: string = "";
export let debounceMs: number = 300;

const dispatch = createEventDispatcher<{ search: string; keydown: KeyboardEvent }>();

let timeout: ReturnType<typeof setTimeout>;

function handleInput() {
clearTimeout(timeout);
timeout = setTimeout(() => {
dispatch("search", value);
}, debounceMs);
}

function handleSubmit() {
clearTimeout(timeout);
dispatch("search", value);
}
</script>

<div class="relative">
<input
type="text"
class="w-full px-3 py-2 text-sm rounded-md border bg-input text-fg-primary placeholder:text-fg-muted focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
{placeholder}
bind:value
on:input={handleInput}
on:keydown={(e) => {
if (e.key === "Enter") handleSubmit();
dispatch("keydown", e);
}}
/>
</div>
Loading
Loading