Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
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
41 changes: 41 additions & 0 deletions web-admin/src/features/admin/billing/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// web-admin/src/features/admin/billing/selectors.ts
import {
adminServiceGetPaymentsPortalURL,
createAdminServiceSudoExtendTrial,
createAdminServiceSudoTriggerBillingRepair,
createAdminServiceSudoDeleteOrganizationBillingIssue,
createAdminServiceSudoUpdateOrganizationBillingCustomer,
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 createBillingRepairMutation() {
return createAdminServiceSudoTriggerBillingRepair();
}

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

export function createSetBillingCustomerMutation() {
return createAdminServiceSudoUpdateOrganizationBillingCustomer();
}

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/admin/layout/AdminPageHeader.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-slate-900">
{title}
</h1>
{#if description}
<p class="text-sm text-slate-500 mt-1">
{description}
</p>
{/if}
</div>
68 changes: 68 additions & 0 deletions web-admin/src/features/admin/layout/AdminSidebar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!-- web-admin/src/features/admin/layout/AdminSidebar.svelte -->
<script lang="ts">
import { page } from "$app/stores";

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

function isActive(href: string, pathname: string): boolean {
if (href === "/-/admin") return pathname === "/-/admin";
return pathname.startsWith(href);
}
</script>

<nav
class="w-56 flex-shrink-0 border-r border-slate-200 bg-slate-50 flex flex-col h-full"
>
<div class="px-4 py-4 border-b border-slate-200">
<span class="text-sm font-semibold text-slate-900">
Admin 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-[11px] font-semibold uppercase tracking-wider text-slate-400 px-2 mb-1 block"
>
{group.heading}
</span>
{#each group.items as item}
<a
href={item.href}
class="block px-2 py-1.5 text-sm rounded-md transition-colors {isActive(
item.href,
$page.url.pathname,
)
? 'bg-slate-100 text-slate-900 font-medium'
: 'text-slate-600 hover:bg-slate-100'}"
>
{item.label}
</a>
{/each}
</div>
{/each}
</div>
</nav>
39 changes: 39 additions & 0 deletions web-admin/src/features/admin/organizations/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// web-admin/src/features/admin/organizations/selectors.ts
import {
createAdminServiceGetOrganization,
createAdminServiceListOrganizationMemberUsers,
createAdminServiceListProjectsForOrganization,
createAdminServiceSearchProjectNames,
} 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 } },
);
}

// 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/admin/projects/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// web-admin/src/features/admin/projects/selectors.ts
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/admin/quotas/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// web-admin/src/features/admin/quotas/selectors.ts
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();
}
71 changes: 71 additions & 0 deletions web-admin/src/features/admin/shared/ConfirmDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!-- web-admin/src/features/admin/shared/ConfirmDialog.svelte -->
<script lang="ts">
export let open = false;
export let title: string;
export let description: string = "";
export let confirmLabel: string = "Confirm";
export let cancelLabel: string = "Cancel";
export let destructive: boolean = false;

let loading = false;

async function handleConfirm() {
loading = true;
try {
await onConfirm();
open = false;
} catch {
// Error handling is the caller's responsibility (via notifyError in onConfirm).
// Keep dialog open so the user can retry or cancel.
} finally {
loading = false;
}
}

function handleCancel() {
open = false;
}

export let onConfirm: () => void | Promise<void>;
</script>

{#if open}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
on:click={handleCancel}
>
<div
class="bg-slate-50 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
on:click|stopPropagation
>
<h2 class="text-lg font-semibold text-slate-900">
{title}
</h2>
{#if description}
<p class="text-sm text-slate-500 mt-2">
{description}
</p>
{/if}
<div class="flex justify-end gap-3 mt-6">
<button
class="px-4 py-2 text-sm rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={handleCancel}
disabled={loading}
>
{cancelLabel}
</button>
<button
class="px-4 py-2 text-sm rounded-md text-white disabled:opacity-50 disabled:cursor-not-allowed {destructive
? 'bg-red-600 hover:bg-red-700'
: 'bg-blue-600 hover:bg-blue-700'}"
on:click={handleConfirm}
disabled={loading}
>
{#if loading}Working...{:else}{confirmLabel}{/if}
</button>
</div>
</div>
</div>
{/if}
92 changes: 92 additions & 0 deletions web-admin/src/features/admin/shared/OrgSearchInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!-- Org name input with search dropdown powered by project name search -->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { searchOrgNames } from "@rilldata/web-admin/features/admin/organizations/selectors";

export let value = "";
export let placeholder = "Organization name";

const dispatch = createEventDispatcher<{ select: string }>();

let showDropdown = false;
let justSelected = false;

$: orgSearchQuery = searchOrgNames(value);
$: orgNames = extractUniqueOrgs($orgSearchQuery.data?.names ?? []);

function extractUniqueOrgs(projectPaths: string[]): string[] {
const orgs = new Set<string>();
for (const path of projectPaths) {
const org = path.split("/")[0];
if (org) orgs.add(org);
}
return [...orgs].sort();
}

function selectOrg(org: string) {
value = org;
showDropdown = false;
justSelected = true;
dispatch("select", org);
}

function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter") {
showDropdown = false;
justSelected = true;
dispatch("select", value);
}
}

function handleInput() {
justSelected = false;
if (value.length >= 3) {
showDropdown = true;
} else {
showDropdown = false;
}
}

function handleBlur() {
// Delay to allow mousedown on dropdown items to fire first
setTimeout(() => {
showDropdown = false;
}, 150);
}

// When new results arrive, show dropdown only if user is actively typing
$: if (orgNames.length > 0 && value.length >= 3 && !justSelected) {
showDropdown = true;
}
</script>

<div class="relative">
<input
type="text"
class="w-full px-3 py-2 text-sm rounded-md border border-slate-300 bg-slate-50 text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
{placeholder}
bind:value
on:keydown={handleKeydown}
on:input={handleInput}
on:blur={handleBlur}
/>
{#if $orgSearchQuery.isFetching && value.length >= 3}
<div
class="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 border-2 border-slate-300 border-t-blue-600 rounded-full animate-spin"
/>
{/if}
{#if showDropdown && orgNames.length > 0}
<div
class="absolute z-10 w-full mt-1 bg-slate-50 border border-slate-200 rounded-md shadow-lg max-h-48 overflow-y-auto"
>
{#each orgNames as org}
<button
class="w-full text-left px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
on:mousedown|preventDefault={() => selectOrg(org)}
>
{org}
</button>
{/each}
</div>
{/if}
</div>
Loading
Loading