Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 1 addition & 30 deletions apps/studio/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { SidebarProvider } from '@/components/ui/sidebar';
import { Toaster } from '@/components/ui/toaster';
import { AiChatPanel } from '@/components/AiChatPanel';
import { ProductionGuardProvider } from '@/components/production-guard';
import { GlobalSidebar } from '@/components/global-sidebar';
import { TopBar } from '@/components/top-bar';
import { PluginRegistryProvider } from '../plugins';
import { builtInPlugins } from '../plugins/built-in';
Expand All @@ -20,31 +19,6 @@ import type { InstalledPackage } from '@objectstack/spec/kernel';
/** Routes that don't require authentication. */
const PUBLIC_ROUTES = new Set(['/login', '/register']);

/**
* Returns true when the current route should render the GlobalSidebar
* (the top-level nav shell) rather than the package-scoped AppSidebar.
*
* Package-scoped routes (`/$package/*` and `/environments/:id/:package/*`)
* have their own AppSidebar injected by their layout component, so the
* GlobalSidebar must be suppressed there to avoid rendering two sidebars.
*/
function isGlobalShellPath(pathname: string): boolean {
if (PUBLIC_ROUTES.has(pathname)) return false;
// Once the user drills into a package under an environment
// (`/environments/:envId/:package/*` where :package is NOT the reserved
// `packages` segment), the package-scoped AppSidebar takes over.
if (/^\/environments\/[^/]+\/(?!packages(?:\/|$))[^/]+/.test(pathname)) {
return false;
}
// /environments (list), /environments/:envId (overview), and
// /environments/:envId/packages (package management) all keep GlobalSidebar.
const globalPrefixes = ['/orgs', '/environments', '/packages', '/api-console'];
if (pathname === '/') return true;
return globalPrefixes.some(
(p) => pathname === p || pathname.startsWith(p + '/'),
);
}

/**
* Routes where an environment selection is NOT required.
* Everything under /environments (list + detail), org mgmt, auth pages.
Expand Down Expand Up @@ -134,10 +108,8 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
return null;
}

// Authenticated layout with TopBar + Sidebar + Content
// Authenticated layout with TopBar + Content
if (user) {
const showGlobalSidebar = isGlobalShellPath(location.pathname);

return (
<SidebarProvider>
<div className="flex min-h-screen w-full flex-col">
Expand All @@ -147,7 +119,6 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
onSelectPackage={handleSelectPackage}
/>
<div className="flex flex-1 w-full overflow-hidden">
{showGlobalSidebar && <GlobalSidebar />}
<main className="flex flex-1 min-w-0 overflow-hidden">
{children}
</main>
Expand Down
21 changes: 13 additions & 8 deletions apps/studio/src/routes/environments.$environmentId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
* {@link useEnvironmentDetail}, causing every downstream API request to
* carry the `X-Environment-Id` header.
* - Render the package-scoped {@link AppSidebar} (metadata tree) for ALL
* routes under `/environments/:envId/*` — overview, package management,
* and the package workspace — so the user sees the metadata list as soon
* as they select an environment.
* routes under `/environments/:envId/*` EXCEPT `/packages` (the package
* management page), where the sidebar would compete with the page content.
* - When no package is selected (URL has no `:package` segment), AppSidebar
* renders environment-wide metadata by passing `packageId: undefined` to
* `client.meta.getItems`.
* - Redirect back to `/environments` when the environment cannot be loaded.
*/

Expand Down Expand Up @@ -112,15 +114,18 @@ function EnvironmentLayoutComponent() {
}
}, [error, navigate]);

// Only render the package-scoped AppSidebar once the user has drilled into
// a specific package. Environment overview and the packages management page
// continue to render the GlobalSidebar from `routes/__root.tsx`.
// Reserved child segments that do NOT get the metadata AppSidebar.
// /packages is the package-management surface; rendering the tree there
// would compete with the page's own content. Everything else — overview
// and the package workspace — shows the sidebar.
const hideSidebar = location.pathname.endsWith(`/environments/${environmentId}/packages`);

return (
<>
{activePackageId && (
{!hideSidebar && (
<AppSidebar
packages={packages}
selectedPackage={selectedPackage}
selectedPackage={selectedPackage} // null when no package chosen → env-wide metadata
onSelectPackage={handleSelectPackage}
environmentId={environmentId}
/>
Expand Down
51 changes: 37 additions & 14 deletions apps/studio/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { createFileRoute } from '@tanstack/react-router';
import { DeveloperOverview } from '../components/DeveloperOverview';
import { usePackages } from '../hooks/usePackages';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useSession } from '@/hooks/useSession';
import { useEnvironments } from '@/hooks/useEnvironments';

function IndexComponent() {
const { packages, selectedPackage } = usePackages();
function IndexRedirect() {
const navigate = useNavigate();
const { user, session, loading: sessionLoading } = useSession();
const { environments, loading: envsLoading } = useEnvironments();

useEffect(() => {
if (sessionLoading || !user) return; // RequireAuth sends to /login

if (!session?.activeOrganizationId) {
navigate({ to: '/orgs' });
return;
}
if (envsLoading) return;

const lastEnvId = localStorage.getItem('objectstack.lastEnvId');
const targetEnv =
(lastEnvId && environments.find((e) => e.id === lastEnvId)) ||
environments.find((e) => e.isDefault) ||
environments[0];

if (targetEnv) {
navigate({
to: '/environments/$environmentId',
params: { environmentId: targetEnv.id },
});
} else {
navigate({ to: '/environments' });
}
}, [user, session, sessionLoading, environments, envsLoading, navigate]);

return (
<main className="flex min-w-0 flex-1 flex-col overflow-hidden bg-background">
<div className="flex flex-1 flex-col overflow-hidden">
<DeveloperOverview
packages={packages}
selectedPackage={selectedPackage}
/>
</div>
</main>
<div className="flex min-h-screen w-full items-center justify-center bg-background">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary" />
</div>
);
}

export const Route = createFileRoute('/')({
component: IndexComponent,
component: IndexRedirect,
});
29 changes: 24 additions & 5 deletions apps/studio/src/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from '@/hooks/use-toast';
import { useSession } from '@/hooks/useSession';
import { useEnvironments } from '@/hooks/useEnvironments';

export const Route = createFileRoute('/login')({
component: LoginPage,
Expand All @@ -17,16 +18,35 @@ export const Route = createFileRoute('/login')({
function LoginPage() {
const navigate = useNavigate();
const client = useClient() as any;
const { user, refresh } = useSession();
const { session, user, refresh } = useSession();
const { environments, loading: envsLoading } = useEnvironments();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [submitting, setSubmitting] = useState(false);

useEffect(() => {
if (user) {
navigate({ to: '/' });
if (!user) return;
if (!session?.activeOrganizationId) {
navigate({ to: '/orgs' });
return;
}
}, [user, navigate]);
if (envsLoading) return;

const lastEnvId = localStorage.getItem('objectstack.lastEnvId');
const targetEnv =
(lastEnvId && environments.find((e) => e.id === lastEnvId)) ||
environments.find((e) => e.isDefault) ||
environments[0];

if (targetEnv) {
navigate({
to: '/environments/$environmentId',
params: { environmentId: targetEnv.id },
});
} else {
navigate({ to: '/environments' });
}
}, [user, session, environments, envsLoading, navigate]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
Expand All @@ -36,7 +56,6 @@ function LoginPage() {
await client.auth.login({ type: 'email', email, password });
await refresh();
toast({ title: 'Welcome back' });
navigate({ to: '/' });
} catch (err) {
toast({
title: 'Sign in failed',
Expand Down
Loading